第5章 自己动手实现HTTP协议
我们知道HTTP协议是在应用层解析内容的,只需要按照它的报文的格式封装和解析数据就可以了,具体的传输还是使用的Socket,在第4章NioServer的基础上自己做一个简单的实现了HTTP协议的例子。
因为HTTP协议是在接收到数据之后才会用到的,所以我们只需要修改NioServer中的Handler就可以了,在修改后的HttpHandler中首先获取到请求报文并打印出报文的头部(包含首行)、请求的方法类型、Url和Http版本,最后将接收到的请求报文信息封装到响应报文的主体中返回给客户端。这里的HttpHandler使用了单独的线程来执行,而且把SelectionKey中操作类型的选择也放在了HttpHandler中,不过具体处理过程和前面的NioServer没有太大的区别,代码如下:
HttpServer:
public class HttpServer {
public static void main(String[] args) throws Exception{
//创建ServerSocketChannel,监听8080端口
ServerSocketChannel ssc=ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(8080));
//设置为非阻塞模式
ssc.configureBlocking(false);
//为ssc注册选择器
Selector selector=Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
//创建处理器
while(true){
// 等待请求,每次等待阻塞3s,超过3s后线程继续向下运行,如果传入0或者不传参数将一直阻塞
if(selector.select(3000)==0){
continue;
}
// 获取待处理的SelectionKey
Iterator<SelectionKey> keyIter=selector.selectedKeys().iterator();
while(keyIter.hasNext()){
SelectionKey key=keyIter.next();
// 启动新线程处理SelectionKey
new Thread(new HttpHandler(key)).run();
// 处理完后,从待处理的SelectionKey迭代器中移除当前所使用的key
keyIter.remove();
}
}
}
}
HttpHander:
public 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();
}
} public void run() {
try {
// 接收到连接请求时
if (key.isAcceptable()) {
handleAccept();
}
// 读数据
if (key.isReadable()) {
handleRead();
}
} catch (IOException ex) {
ex.printStackTrace();
}
} }
整个过程非常简单,按照报文的格式来读取和发送就可以了,接收到数据后按“\r\n”分割成每一行,在空行之前都是报文头(包含首行),空行下面如果有内容就是报文的主体,因为这里是Get请求所以就没有主体了,首行使用空格分割后可以得到请求的方法、Url和Http的版本,如果需要请求头的值只需要把头部的每一行用冒号分割开就行了。下面就来看一下运行效果,首先启动程序,然后在浏览器中输入http://localhost:8080/发起请求,这时控制台就会打印出如下信息(不同的环境打印的结果会不同)。
控制台报文显示:
浏览器报文显示:
这里的功能并不能真正处理请求,实际处理中应该根据不同的Url和不同的请求方法进行不同的处理并返回不同的响应报文,另外这里的请求报文也必须在bufferSize(1024)范围内,如果太长就会接收不全,而且也不能返回图片等流类型的数据(流类型只需要在响应报文中写清楚Content-Type的类型,并将相应数据写入报文的主体就可以了)。