在网络协议中,Socket是连接应用层和运输层的中间层,主要作用为了通信。Http协议是应用层上的封装协议。我们可以通过Http协议的规范解析Socket中数据,完成Http通信。
首先,我们先回顾一下Http协议的规范。主要复习一下,请求与响应报文格式,方便我们解析Socket中数据。请求报文格式具体如下图:
响应报文格式具体如下图:
了解Http请求和响应的报文格式后,可准备编写代码了。Java的Socket支持BIO、NIO等IO模型,我以下的代码使用BIO阻塞模式实现通信,具体代码如下:
HttpBioServer类主要负责开启Socket服务端监听,当有客户端连接接入后,读取客户端数据,将客户端数据交给另一个线程处理。另一个线程会调用http(),http()会解析Http请求的数据,对数据进行一些操作后,再封装一个响应体返回给客户端。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;/*** 〈一句话功能简述〉<br>* 〈Bio服务端〉** @author hanxiaozhang* @create 2023/6/20* @since 1.0.0*/
public class HttpBioServer {public static void main(String[] args) throws Exception {ServerSocket server = new ServerSocket(9090, 20);while (true) {// 阻塞1Socket client = server.accept();
// System.out.println(client.getInetAddress());
// System.out.println(client.getLocalPort());System.out.println("client connect success,client port is " + client.getPort());new Thread(new Runnable() {@Overridepublic void run() {try {http(client);client.close();System.out.println("client close");} catch (IOException e) {e.printStackTrace();}}}).start();}}/*** Http协议** @param client* @throws IOException*/private static void http(Socket client) throws IOException {// 读取输入流中数据ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();try {InputStream in = client.getInputStream();int len = 0;byte[] buf = new byte[1024];// 每次读取 1024 字节,知道读取完成while ((len = in.read(buf)) != 0) {byteArrayOut.write(buf, 0, len);if (in.available() == 0) {break;}}byte[] bytes = byteArrayOut.toByteArray();RequestEntity request = new RequestEntity();request.byteToRequest(bytes);byte[] responseBytes = handler(request);OutputStream ops = client.getOutputStream();ops.write(responseBytes);ops.flush();} finally {byteArrayOut.close();}}private static byte[] handler(RequestEntity request) {System.out.println("request is " + request);Map<String, String> headers = new HashMap<>(4);headers.put("Content-Type", "text/plain");String body = "success";// 假装处理一些逻辑ResponseEntity response = new ResponseEntity(200, "OK", headers, body);byte[] responseBytes = response.responseToBytes(request);System.out.println("response is " + response);return responseBytes;}}
RequestEntity类主要是存储Http请求解析后的数据,并且包含了一个将请求数据转换为RequestEntity类数据的方法。
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;/*** 〈一句话功能简述〉<br>* 〈请求实体〉** @author hanxiaozhang* @create 2023/6/25* @since 1.0.0*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class RequestEntity {/*** 请求行*/private String requestLine;/*** 请求方法*/private String method;/*** Url*/private String url;/*** 版本协议*/private String requestAndVersion;/*** header*/private Map<String, String> headers;/*** 报文内容*/private String body;/*** 字节数组转换request实体** @param bytes* @return*/public void byteToRequest(byte[] bytes) {// \r\n连续出现两次的情况认为首部结束,剩下是主体部分int flag = 0;// 是否为body内容boolean isBody = false;char temp;StringBuffer headerSb = new StringBuffer(),bodySb = new StringBuffer();Map<String, String> headers = new HashMap<>(16);// 解析请求报文头和请求bodyfor (int i = 0; i < bytes.length; i++) {if (isBody) {bodySb.append((char) bytes[i]);} else {temp = (char) bytes[i];if (temp == '\r' || temp == '\n') {flag++;} else {flag = 0;}if (flag == 4) {isBody = true;}headerSb.append(temp);}}// 解析请求行String[] lines = headerSb.toString().split("\r\n");String requestLine = lines[0];String[] requestLines = requestLine.split("\\s");this.setRequestLine(requestLine).setMethod(requestLines[0]).setUrl(requestLines[1]).setRequestAndVersion(requestLines[2]);// 解析请求headerfor (int i = 1; i < lines.length; i++) {if (lines[i] != "") {String[] header = lines[i].split(": ");headers.put(header[0], header[1]);}}this.setHeaders(headers).setBody(bodySb.toString());}}
ResponseEntity类主要是存储Http需要响应的数据,并且包含了一个将ResponseEntity类数据转换成Http响应的方法。
import com.hanxiaozhang.utils.StringUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;import java.util.HashMap;
import java.util.Map;/*** 〈一句话功能简述〉<br>* 〈〉** @author hanxiaozhang* @create 2023/6/25* @since 1.0.0*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class ResponseEntity {public ResponseEntity(Integer stateCode, String reason, Map<String, String> headers, String body) {this.stateCode = stateCode;this.reason = reason;this.headers = headers;this.body = body;}/*** 状态*/private String stateLine;/*** 版本协议*/private String requestAndVersion;/*** 状态码*/private Integer stateCode;/*** 原因*/private String reason;/*** 响应header*/private Map<String, String> headers;/*** 响应内容*/private String body;public byte[] responseToBytes(RequestEntity request) {// 处理状态行StringBuilder sb = new StringBuilder();this.requestAndVersion = request.getRequestAndVersion();this.stateLine = request.getRequestAndVersion() + " " + stateCode + " " + reason;sb.append(stateLine);sb.append("\r\n");// 处理响应headerMap<String, String> tempHeaders = new HashMap<>(16);tempHeaders.putAll(request.getHeaders());if (this.headers != null && !this.headers.isEmpty()) {tempHeaders.putAll(this.headers);}if (StringUtil.isNotBlank(this.body)) {tempHeaders.put("Content-Length", String.valueOf(this.body.length()));}tempHeaders.forEach((k, v) -> {sb.append(k + ": " + v + "\r\n");});sb.append("\r\n");// 处理响应bodyif (StringUtil.isNotBlank(this.body)) {sb.append(this.body);}return sb.toString().getBytes();}}
最后,我们启动HttpBioServer类,在浏览器中地址栏请求该地址,看一下效果: