前言
今天突然想到手写一个可以和http通信的简易ServerSocket,但是在完成这个功能时遇到了一些困难,故在此做个分享
实现
直接上代码吧,第一个版本代码如下
public class MyServlet2 {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8081);
while(true) {
Socket clientSocket = serverSocket.accept();
InputStream inputStream = clientSocket.getInputStream();
byte[] bytes = new byte[1024];
int length;
String str = "";
// 核心代码
while((length = inputStream.read(bytes)) > 0) {
str += new String(bytes, 0, length, "UTF-8");
}
System.out.println(str);
}
}
}
这个版本一开始看上去好像没什么问题,但是当我通过浏览器输入http://localhost:8081/123时发现线程被阻塞了,然后最终定位到线程阻塞在while((length = inputStream.read(bytes)) > 0) 这一行,然后我查看以前读取文件流的方式,看了很久也没发现异常,没办法了只能去查阅相关资料
最终定位到的问题是,http和Socket通信时,服务端不知道客户端什么时候发送数据结束,所以执行inputStream.read时会一直等待,所以会阻塞
然后查询相关资料可解决方案有以下几种
1、设置客户端超时时间,超过指定时间则break循环,但是这种方式肯定是不靠谱的,总不能不让客户端长时间传输数据吧
2、根据http协议自己判断客户端什么时候发送数据
http协议规范
这里先普及下http协议的几个规范
1、http协议http头的结束必定是以结束符号\r\n\r\n结束(即两个回车符号)
2、如果请求包含请求体则http会存在字段Content-Length,如Content-Length :20(注意中间还有个空格),20表示请求体的字节长度
3、http请求头是和请求体连在一起传输的
ps(据说还有请求体内容不确定的,本文暂不考虑这种情况吧)
根据以上规范,2.0版本的代码如下
/**
* @description 实现一个简易的http和servlet通信功能
* http协议规则:
* http头传输结束符号\r\n\r\n(即两个回车符号)
* 有消息体则存在Content-Length字段,长度为body的字节数量
*
*/
public class MyServlet {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8081);
while(true) {
Socket clientSocket = serverSocket.accept();
InputStream inputStream = clientSocket.getInputStream();
// 缓冲区设置比较小,便于测试
byte[] bytes = new byte[10];
int length;
byte[] allBytesArray = new byte[]{};
// 核心代码,需要判断什么时候客户端请求发送结束
while((length = inputStream.read(bytes)) > 0) {
// 数组合并,使用字节累加,直接转String的话中文会被分割,导致乱码
byte[] joinedArray = new byte[allBytesArray.length + length];
System.arraycopy(allBytesArray, 0, joinedArray, 0, allBytesArray.length);
System.arraycopy(bytes, 0, joinedArray, allBytesArray.length, length);
allBytesArray = joinedArray;
String str = new String(allBytesArray, "UTF-8");
// 是否有消息体
boolean existBody = str.contains("Content-Length");
if(!existBody && str.lastIndexOf("\r\n\r\n") != -1) {
// 没有body体,则根据http协议,http header头结尾为\r\n\r\n,结尾是\r\n\r\n时判断数据接收完成
break;
}
// 有请求体并且http已经接收完成
if(existBody && str.contains("\r\n\r\n")) {
System.out.println("-------------------存在body");
// 先等http头传输完成,截取头内容
String header = str.substring(0,str.indexOf("\r\n\r\n"));
System.out.println("-------------------header:" + header);
// 读取Content-Length的长度
int startIndex = header.indexOf("Content-Length");
int start = header.indexOf(" ",startIndex);
int end = header.indexOf("\r\n",startIndex);
// 截取请求体长度
int contentLength = Integer.valueOf(header.substring(start+1,end));
System.out.println("-------------------contentLength:" + contentLength);
int total = str.getBytes().length;
// 当前字节长度
int currentBodyLength = total - header.getBytes().length - "\r\n\r\n".getBytes().length;
System.out.println("-------------------currentBodyLength:" + currentBodyLength);
if(currentBodyLength == contentLength) {
//当前长度达到Content-Length表示接收完成
break;
}
}
}
OutputStream outputStream = clientSocket.getOutputStream();
outputStream.
write(("HTTP/1.1 200 OK\r\n" + //响应头第一行
"Content-Type: text/html; charset=utf-8\r\n" + //简单放一个头部信息
"\r\n" + //这个空行是来分隔请求头与请求体的
"<h1>hello world</h1>\r\n").getBytes());
outputStream.close();
inputStream.close();
clientSocket.close();
}
}
}
经过测试,传递参数和不传递参数均可以正常使用。
本次2.0版本是基于BIO,并且只有单个线程处理,关闭流等方式也都是直接往外抛异常的,还可以使用线程池升级处理请求方式,或者使用Nio来实现会更好