1、背景
最近看了OkHttp源码,用的责任链模式,通过一些列拦截器(如重定向、缓存、网络链接等)实现网络请求,服务器连接部分是通过Socket,因此手动实现一下,加深记忆。
2、HTTP请求格式
贴一张图,来自HTTP请求格式
这个图就是实现的核心,通过Socket的输出流,把上述格式的字符串数据传给服务器,再通过输入流读取服务器的响应即可。
3、具体实现
3、1 封装一下请求
/**
* Description: http请求封装
* Author : pxq
* Date : 19-12-28 下午5:34
*/
public class Request {
//路径对应的URL
private URL url;
//原本请求的路径
private String path;
//请求协议
private String protocol;
//请求端口
private int port;
//请求主机
private String host;
//默认get请求
private Method method = Method.GET;
//额外的请求头
private Map<String, String> headers;
//额外的请求参数
private Map<String, String> params;
public Request(String url){
init(url);
}
private void init(String path) {
try {
this.path = path;
this.url = new URL(path);
this.port = getDefaultPort(this.url);
this.host = this.url.getHost();
this.protocol = this.url.getProtocol();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
private static int getDefaultPort(URL url){
if (url.getPort() != -1){
return url.getPort();
}
//https
if ("https".equals(url.getProtocol())){
return 443;
}
//http
return 80;
}
.......getter setter省略.......
}
当传入一个api路径时,比如http://www.xxxxx.com/ 可以通过URL去解析这个路径拿到它的协议、端口、host等信息。需要注意的是当路径里没有显示的指明端口时,会返回-1,这里做了简单处理:http默认端口80和https默认端口443。
3、2 实现Socket请求
首先创建Socket,https用SSLSocketFactory创建,http直接new Socket()即可,OkHttp做了Socket重用的,这里没做。
/**
* Description: 一个socket池,可以做socket缓存重用
* Author : pxq
* Date : 19-12-28 下午5:49
*/
public class SocketPool {
/**
* 获取socket
* @param request
* @return
* @throws IOException
*/
public Socket getSocket(Request request) throws IOException {
if ("https".equals(request.getProtocol())){
return SSLSocketFactory.getDefault().createSocket(request.getHost(), request.getPort());
}
return new Socket(request.getHost(), request.getPort());
}
}
创建之后,就已经完成了与服务器的三次握手,接下来只要按http请求格式完成数据读写即可。需要注意的是,有些api需要 User-Agent这个请求头,有些不需要,而且这个请求头还不能乱写…
/**
* Description: 同步请求
* Author : pxq
* Date : 19-12-28 下午5:52
*/
public class Call {
private HttpClient mHttpClient;
private Request mRequest;
public Call(HttpClient client, Request request) {
mRequest = request;
mHttpClient = client;
}
public Response call() throws IOException {
String httpPath = mRequest.getUrl().getPath();
String query = handleParams();
if (mRequest.getMethod() == Method.GET) {
if (query.length() != 0) {
httpPath += "?" + query;
}
}
StringBuilder httpData = new StringBuilder();
getHttpLine(httpData, httpPath);
getHttpHeaders(httpData);
getHttpBody(httpData, query);
//执行网络请求
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
Socket socket = null;
try {
long start = System.currentTimeMillis();
socket = mHttpClient.getSocketPool().getSocket(mRequest);
System.out.println(System.currentTimeMillis() - start);
OutputStream outputStream = socket.getOutputStream();
bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
//把请求数据传给服务器
String requestData = httpData.toString();
System.out.println(requestData);
/*
* GET http://gank.io/api/data/Android/10/1 HTTP/1.1
User-Agent:sockettest/1.1
Host:gank.io
*/
System.out.println("request data end..");
bufferedWriter.write(requestData);
bufferedWriter.flush();
//获取服务器返回的数据
StringBuilder response = new StringBuilder();
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String temp;
while ((temp = bufferedReader.readLine()) != null) {
response.append(temp);
response.append("\n");
}
return new Response(response.toString());
} finally {
if (bufferedWriter != null) {
bufferedWriter.close();
}
if (bufferedReader != null) {
bufferedReader.close();
}
if (socket != null) {
socket.close();
}
}
}
/**
* 获取请求行
*
* @param httpRequest 入参
*/
private void getHttpLine(StringBuilder httpRequest, String path) {
//line
httpRequest.append(mRequest.getMethod().value());
httpRequest.append(" ");
//拼接完整的请求路径
httpRequest.append(mRequest.getProtocol());
httpRequest.append("://");
//host
httpRequest.append(mRequest.getHost());
if (path.length() == 0){
path = "/";
}
httpRequest.append(path);
httpRequest.append(" ");
httpRequest.append("HTTP/1.1");
httpRequest.append("\r\n");
}
/**
* 把传的headers map转为http请求头
*
* @param httpRequest 入参
*/
private void getHttpHeaders(StringBuilder httpRequest) {
getHeader(httpRequest, createHeaderMap());
}
/**
* 处理post请求的请求体
*
* @param httpRequest
* @param params
*/
private void getHttpBody(StringBuilder httpRequest, String params) {
if (mRequest.getMethod() == Method.POST && params != null && params.length() > 0) {
httpRequest.append(params);
httpRequest.append("\r\n");
}
}
/**
* 把传入的请求参数转为http请求体,包括请求path上的参数
*
* @return 所有的请求参数字符串
*/
private String handleParams() {
StringBuilder paramsBuilder = new StringBuilder();
//获取请求path上的参数
String query = mRequest.getUrl().getQuery();
if (query != null) {
paramsBuilder.append(query);
}
//获取额外的参数
Map<String, String> params = mRequest.getParams();
if (params != null) {
paramsBuilder.append("&");
for (Map.Entry<String, String> keyAndValue : params.entrySet()) {
paramsBuilder.append("&");
paramsBuilder.append(keyAndValue.getKey());
paramsBuilder.append("=");
paramsBuilder.append(keyAndValue.getValue());
}
}
return paramsBuilder.toString();
}
/**
* 添加默认的请求头,并把传入的请求头键值对返回
*
* @return 所有的请求头键值对
*/
private Map<String, String> createHeaderMap() {
//请求头
Map<String, String> headerMap = mRequest.getHeaders();
if (headerMap == null) {
headerMap = new HashMap<>();
}
//主机
headerMap.put("Host", mRequest.getHost());
//代理 有些api没有这个请求头不让访问
headerMap.put("User-Agent", "SocketRuntime/1.0");
//Connection
// headerMap.put("Connection", "keep-alive");
//Accept-Encoding 压缩传输
// headerMap.put("Accept-Encoding", "gzip, deflate");
return headerMap;
}
/**
* 把请求头键值对转为http请求头
*
* @param httpRequest 入参
* @param headerMap 请求头键值对
*/
private void getHeader(StringBuilder httpRequest, Map<String, String> headerMap) {
if (headerMap == null) {
return;
}
for (Map.Entry<String, String> keyAndValue : headerMap.entrySet()) {
httpRequest.append(keyAndValue.getKey());
httpRequest.append(":");
httpRequest.append(keyAndValue.getValue());
httpRequest.append("\r\n");
}
//空一行,代表请求头完结,之后的内容是请求体
httpRequest.append("\r\n");
}
}
4、实现效果
用gank.io提供的api测试一下:
/**
* Description: 测试Socket实现的get请求
* Author : pxq
* Date : 19-12-28 下午5:59
*/
public class HttpTest {
public static void main(String[] args) {
HttpClient client = new HttpClient();
String path = "http://gank.io/api/data/Android/10/1";
final Request request = new Request(path);
client.execute(request, new CallBack() {
@Override
public void onSuccess(Response response) {
System.out.println(response.string());
}
@Override
public void onError(Throwable throwable) {
}
});
}
}
效果图:
这里打印了请求信息和返回的所有信息。还有响应也没处理,直接打印所有的返回了…
5、最后
虽然实现了,但是请求很慢(Socket连接都要好几秒)…不知道为什么,可能得好好了解http之后才懂吧。