图源:
HTTP协议实际上是基于TCP的应用层协议,规定了请求报文和响应报文等。参考HTTP协议的规定,我们可以实现一个简单的HTTP Server。
前文中我介绍了如何实现简单的TCP服务端和客户端,本文将在之前示例的基础上实现一个简单的HTTP服务。
HTTP的基本概念推荐阅读,或者。
请求报文
创建一个类负责解析并结构化请求报文:
package cn.icexmoon.java.note.ch25;
// ...
public class HttpRequest {
private String headLine;
private Map<String, List<String>> headers = new LinkedHashMap<>();
private String body;
private String httpMethod;
private String url;
private String schema;
public HttpRequest(BufferedReader br) throws IOException {
//处理首行
headLine = br.readLine();
String[] hlPart = headLine.split(" ");
if (hlPart.length < 3) {
throw new RuntimeException("请求报文首行格式错误");
}
httpMethod = hlPart[0];
url = hlPart[1];
schema = hlPart[2];
//处理报文头
boolean requestReadEnd = false;
do {
String header = br.readLine();
if (header == null) {
//报文读取结束
requestReadEnd = true;
break;
}
if (header.isEmpty()) {
//报文头结束
break;
}
int separatorIdx = header.indexOf(":");
if (separatorIdx <= 0) {
throw new RuntimeException("报文头格式错误,缺少分隔符");
}
String headerKey = header.substring(0, separatorIdx);
String headerValues;
try {
headerValues = header.substring(separatorIdx + 1);
headerValues = headerValues.trim();
} catch (IndexOutOfBoundsException e) {
e.printStackTrace();
throw new RuntimeException("报文头格式错误,缺少值");
}
if (headerValues == null || headerValues.isEmpty()) {
this.headers.put(headerKey, null);
}
else{
String[] values = headerValues.split(",");
List<String> valuesList = Arrays.stream(values).map(v -> v.trim()).collect(Collectors.toList());
this.headers.put(headerKey, valuesList);
}
}
while (true);
if (requestReadEnd) {
this.body = "";
return;
}
//处理报文体
String contentLength = getFirstHeaderVal("Content-Length");
if (contentLength != null) {
if (Integer.valueOf(contentLength).compareTo(0) <= 0) {
this.body = "";
return;
}
}
StringBuilder sb = new StringBuilder();
final int SIZE = Integer.valueOf(contentLength);
char[] bodyBf = new char[SIZE];
br.read(bodyBf);
this.body = new String(bodyBf);
}
/**
* 获取第一个报文头的值
*
* @param headerKey 报文头的key
* @return 如果缺少报文头或者报文头没有值,返回null
*/
private String getFirstHeaderVal(String headerKey) {
if (!headers.containsKey(headerKey)) {
return null;
}
if (headers.get(headerKey) == null) {
return null;
}
return headers.get(headerKey).get(0);
}
}
HttpRequest
的构造器接受一个输入流,并从流中读取请求报文并解析。
需要注意的是,报文体最后没有换行符,所以不能通过readLine
方法读取。这是有意义的,因为除了常见的通过报文体传递字符串外,二进制文件也可以通过报文体传输,所以报文体必须按照字节方式读取,字节长度由报文头Content-Length
指定。
响应报文
一次完整的HTTP请求由请求和响应构成,所以这里同样需要构建响应报文:
package cn.icexmoon.java.note.ch25;
// ...
chain = true)
(
public class HttpResponse {
private String schema;
private Integer code;
private String desc;
private Map<String, List<String>> headers = new LinkedHashMap<>();
private String body;
public HttpResponse setCode(Integer code) {
this.code = code;
if (code.equals(200)) {
desc = "ok";
}
return this;
}
/**
* 添加响应报文头,同一个报文头可以调用多次添加多个值
* @param key
* @param value
*/
public void addHeader(String key, String value){
if (!headers.containsKey(key)){
headers.put(key, new LinkedList<>());
}
List<String> values = headers.get(key);
values.add(value);
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(schema).append(" ").append(code).append(" ").append(desc).append("\n");
if (headers != null) {
headers.forEach((key, values) -> {
sb.append(key).append(":");
values.forEach(v -> {
sb.append(v).append(",");
});
if (sb.charAt(sb.length() - 1) == ',') {
sb.deleteCharAt(sb.length() - 1);
}
sb.append("\n");
});
}
sb.append("\n");
if (body != null && !body.isEmpty()) {
sb.append(body);
}
return sb.toString();
}
public static HttpResponse buildSuccessResponse() {
return new HttpResponse()
.setCode(200)
.setDesc("ok")
.setSchema("HTTP/1.1")
.setBody("");
}
}
响应报文相对简单,这里没什么需要过多说明的。
HTTP服务
修改前文的TCP服务端代码,分析请求报文并解析为HttpRequest
对象:
package cn.icexmoon.java.note.ch25;
// ...
public class Main {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(6666);
System.out.println("server is starting...");
ExecutorService es = Executors.newCachedThreadPool();
while (true) {
Socket client = ss.accept();
final Socket finalClient = client;
es.execute(new Runnable() {
public void run() {
try {
System.out.println("get connection from " + finalClient.getRemoteSocketAddress());
dealRequest(finalClient);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
private static void dealRequest(Socket clientSocket) throws IOException {
String addr = clientSocket.getRemoteSocketAddress().toString();
try {
InputStream is = clientSocket.getInputStream();
OutputStream os = clientSocket.getOutputStream();
InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
HttpRequest httpRequest = new HttpRequest(br);
System.out.println("=============http request=========");
System.out.println("http method:" + httpRequest.getHttpMethod());
System.out.println("url:" + httpRequest.getUrl());
System.out.println("schema:" + httpRequest.getSchema());
System.out.println("============header==========");
httpRequest.getHeaders().forEach((key, values) -> {
System.out.println(key + ":" + values);
});
System.out.println("==========body============");
System.out.println(httpRequest.getBody());
//返回响应报文
String bodyStr = "{\"code\":401,\"data\":null,\"msg\":\"你好\"}";
int length = bodyStr.getBytes(StandardCharsets.UTF_8).length;
HttpResponse httpResponse = HttpResponse.buildSuccessResponse();
httpResponse.addHeader("Content-Type", "application/json;charset=utf-8");
httpResponse.addHeader("Content-Length", Integer.valueOf(length).toString());
httpResponse.addHeader("Keep-Alive", "timeout=60");
httpResponse.setBody(bodyStr);
bw.write(httpResponse.toString());
bw.flush();
System.out.println("========response=============");
System.out.println(httpResponse.toString());
} catch (Exception e) {
System.out.println(String.format("[%s]connect is closed by IOException.", addr));
e.printStackTrace();
} finally {
clientSocket.close();
System.out.println(String.format("[]connect is closed normally.", addr));
}
}
private static void printWholeRequest(BufferedReader br) throws IOException {
do {
String line = br.readLine();
if (line == null) {
break;
}
System.out.println(line);
}
while (true);
}
}
这里没有根据不同请求进行进一步处理,仅是将请求报文解析后在服务端控制台简单打印,并最终返回一个简单的JSON格式响应。
现在可以将程序打包并运行了,客户端使用浏览器或者接口调试工具都可。
如果使用mvn打包时出现编码GBK的不可映射字符之类的错误,可以检查是否在pom中指定Java编译时的源码编码
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
,具体可以参考。如果控制台或接口调用工具输出乱码,可以检查是否在字节流和字符流转换部分指定编码,比如:
new InputStreamReader(is, StandardCharsets.UTF_8)
。
最后,附上我的测试结果:
谢谢阅读。
本文的最终完整示例可以从获取。
文章评论