java I/O体系总结(四) 使用socket构建HTTP服务器
前言
在 java I/O体系总结(三)中,已使用阻塞和非阻塞的方式分别构建了一个简单的服务器。HTTP作为Web浏览器和Web服务器之间通信的标准协议,在网络编程中有也重要位置。下面就深入HTTP协议,尝试使用NIO构建简单的HTTP服务器。
HTTP协议
我们都知道,HTTP协议是应用层协议,全称超文本传输协议。构建于TCP/IP之上。默认使用80端口通信。一般情况下的HTTP协议是由客户端发起,服务端响应的模式。HTTP协议还是一个无状态协议,同一个客户端的某此请求和其上一次请求无关。
HTTP请求
HTTP格式分为请求和响应。两个形式格式不同。请求一般由客户端发起,表明向服务器请求某项资源。响应由服务器做出,用来响应客户端。
请求格式为 : 请求行+请求首部+空行+请求消息体
必须有的是请求行,首部和消息体不一定有。
示例:
POST /index HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Connection: keep-alive
Content-Length: 308
Content-Type: text/plain;charset=UTF-8
[{"headers":{"component":"enterprise","datatype":"re","version":"v1"},"body":"{\"re\":\"uid=wthfeng&ref=https%3A%2F%2Fblog.youkuaiyun.com%2F&pid=blog&mod=popu_4&dsm=get&mtp=2&con=&ck=-&curl=https%3A%2F%2Fblog.youkuaiyun.com%2Fwthfeng&session_id=10_1531129353425.349656&x-acl-token=status_js_dkuyqthzbajmncbsb_token\"}"}]
第一行即请求行,必须有。在此例中为POST /index HTTP/1.1
。post表明请求方法,/index表示请求地址;HTTP/1.1表示http版本。余下类似User-Agent、Connection
等为请求首部。空行之后,就是请求体正文。
HTTP响应
HTTP响应与请求类似,同样有一定格式。主要包括: 状态行+响应首部+空行+响应正文。
如下:
HTTP/1.1 200 OK
Server: openresty
Date: Fri, 14 Sep 2018 04:24:37 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Keep-Alive: timeout=20
Vary: Accept-Encoding
Content-Encoding: gzip
Strict-Transport-Security: max-age= 31536000
<html>
...
/html>
HTTP/1.1 200 OK
即是状态行。200为状态码,表明此次请求是成功的。HTTP/1.1为http版本。后续为响应header。空行之后为响应的资源,这里是响应的html页面。
服务器
服务器可说的不多,主要在于怎样使用NIO构建一个http响应。当然这里并不是一个真正的http服务器,只是任何请求都返回了一个百度页面。此案例参考了《java网络编程 第4版》第9章。
public class NioHttpServer {
public static void main(String[] args) throws Exception {
int port = 8080;
String encoding = "UTF-8";
AtomicInteger totalNum = new AtomicInteger(0);
Path path = Paths.get("/Users/wangtonghe/Desktop/baidu.html");
byte[] data = Files.readAllBytes(path);
String contentType = "text/html";
// http的响应header
String header =
// 状态行
"HTTP/1.1 200 OK\n" +
"Server: OneFile 2.0\n" +
"Content-length: " + data.length + "\r\n" +
"Content-type: " + contentType + ",charset=" + encoding
// 空行
+ "\r\n\r\n";
// 创建 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置非阻塞模式
serverSocketChannel.configureBlocking(false);
// 绑定到本地端口
serverSocketChannel.bind(new InetSocketAddress(port));
Selector selector = Selector.open();
// 注册事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer readBuf = ByteBuffer.allocate(50);
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 删除,避免处理多次
iterator.remove();
//接收通道已准备好(即服务端有数据过来)
if (selectionKey.isAcceptable()) {
System.out.println("接受请求数:" + totalNum.incrementAndGet());
// 获取关联的ServerSocketChannel(因为是服务器channel)
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
// 这里是不阻塞的,获取客户端的
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ);
// 客户端有可读事件
} else if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
readBuf.clear();
StringBuilder strBuilder = new StringBuilder();
while (socketChannel.read(readBuf) > 0) {
readBuf.flip();
while (readBuf.hasRemaining()) {
strBuilder.append((char) readBuf.get());
}
readBuf.clear();
}
System.out.println(strBuilder.toString());
} else if (selectionKey.isWritable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer dataBuf = ByteBuffer.wrap(data);
ByteBuffer headerBuf = ByteBuffer.wrap(header.getBytes());
//
dataBuf.rewind();
headerBuf.rewind();
// 向客户端写http的header
socketChannel.write(headerBuf);
// 向客户端写http的响应正文
socketChannel.write(dataBuf);
// 处理完后关闭连接
socketChannel.close();
}
}
}
}
}
总结
从此示例中,算是真正知道了http的响应规则。之前知道http有header头和正文,却不知道是怎样关联和怎样输出。其实也是,各种协议之间也就是通过传输规则和位置确定的。
不过有个问题是,当我用jmeter压测这个服务器时,效果却不很理想。当达到上千TPS时,错误率高的惊人。怀疑写的有问题或哪里不对。还想各位指正哈。