本篇文章是okhttp介绍的第二篇,如要复习http理论基础知识,参:一、http 理论基础
本篇主要是okhttp主要部分源码与逻辑。
基于socket创建一个http
首先通过上篇的文章,我们了解了tcp与http协议,而在java中,如果我们要创建一个tcp连接,则可以使用Socket(套接字),socket 是java为我们创建与建立tcp连接而提供的一套api,注:(创建udp则是DatagramSocket)。那么这里称socket连接就是tcp连接,下同。建立socket连接后我们通过输出流发送指定http数据格式,就能创建一个http请求了,然后获取输入流,读取数据,获取响应结果。
package com.wzh.server;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
public class HelloWorld extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String queryStr = req.getQueryString();
resp.setContentType("text/html");
resp.setHeader("head", "value");
//设置状态码
resp.setStatus(200);
PrintWriter out = resp.getWriter();
out.println("Hello world, this message is from servlet get!");
out.println("query:" + queryStr);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cookie[] cookies = req.getCookies();//获取cookie
String protocol = req.getProtocol();//协议版本
Enumeration<String> headerNames = req.getHeaderNames();//header
String query = req.getQueryString();//拼接在url后的请求参数
resp.setContentType("text/html;charset=utf-8");//设置响应内容类型
resp.setStatus(200);
resp.addHeader("respHead", "v1");//添加头
resp.addCookie(new Cookie("cookie1", "value1"));//添加cookie
PrintWriter writer = resp.getWriter();
writer.println("protocol :" + protocol);
writer.println("your cookie start-------");
if (cookies != null)
for (Cookie c : cookies)
writer.println(c.getName() + "=" + c.getValue());
writer.println("cookie end-------");
writer.println("your header start-------");
if (headerNames != null)
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
writer.println(key + ":" + req.getHeader(key));
}
writer.println("your header end-------");
writer.println("query:" + query);
BufferedReader reader = req.getReader();//获取body
String body = "";
String s;
while ((s = reader.readLine()) != null) {
body += s + "\r\n";
}
reader.close();
writer.println("body:" + body);
writer.close();
}
}
这是一个简单的servlet,搭配个Tomcat,服务启动,浏览器能访问就是正常了。这里重点不是服务器,而是客户端,http服务器可以是其它任何服务器。主要有doGet,doPost两个方法,当http请求进来时,会进相应的方法进行处理,这里说下doPost函数。可以看到主要是把客户端传上来的cookie,header,与body信息都返回给客户端,代码比较简单,应该能看懂,不懂请查jsp 相关文章。接下来看客户端发送一个get请求
public class SocketTest {
public static final String IP = "192.168.31.236";
public static final int PORT = 8080;
public static void main(String[] args) throws IOException {
httpGetTest();
// httpPostTest();
// httpPostOkioTest();
}
private static void httpGetTest() throws IOException {
Socket socket = new Socket(IP, PORT);
socket.setKeepAlive(true);
socket.setSoTimeout(100 * 1000);
OutputStream os = socket.getOutputStream();
String protocol = "GET /WebServerTest_war_exploded/HelloWorld?a=b&c=d HTTP/1.1\r\n";//必须
os.write(protocol.getBytes());
String header = "Host: " + IP + ":" + PORT + "\r\n" +//必须
"ContentType:application/x-www-form-urlencoded\r\n" +
"Content-Length: 0\r\n" +
"Connection: Keep-Alive\r\n" +
"head1:Value1\r\n";
os.write(header.getBytes());
os.write("\r\n".getBytes());
InputStream is = socket.getInputStream();
String resp = readInputStream(is);
System.out.println(resp);
socket.close();
}
private static String readInputStream(InputStream is) throws IOException {
byte[] bytes = new byte[1024];
int i;
String str = "";
while ((i = is.read(bytes)) != -1) {
str += new String(bytes, 0, i);
}
return str;
}
}
使用ip,端口创建socket,设置超时时间,获取 输出流,然后先write了 请求行(Request Line),就是GET /WebServerTest_war_exploded/HelloWorld?a=b&c=d HTTP/1.1\r\n
这一行,按 请求方法 GET空格请求地址 /WebServerTest_war_exploded/HelloWorld?a=b&c=d空格 协议版本号 HTTP/1.1,以\r\n换行符结束。然后write 请求头(Request Header),head中以key:空格value的形式,换行符分隔,其中Host 头是必须添加的,最后write一个换行符结束发送,然后获取输入流,读取数据,打印后如下:
HTTP/1.1 200
head: value
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 61
Date: Tue, 23 Jul 2019 16:35:58 GMT
Hello world, this message is from servlet get!
query:a=b&c=d
这就是服务器返回的所有数据,第一行称为 响应行 包含服务器协议与 状态码,然后是 响应head ,在服务器中添加了一个自定义head,其它head为服务器底层自动添加,然后 head 空行之后就是 响应body 也就是我们常拿数据的地方,这是返回一句话,并把客户端传的请求参数返回 回来。这里没有对服务器返回解析为具体的code,head,body,其实就是字符串处理了,按照指定规则解析数据就行了。接下来看Post.
private static void httpPostTest() throws IOException {
Socket socket = new Socket(IP, PORT);
OutputStream os = socket.getOutputStream();
String protocol = "POST /WebServerTest_war_exploded/HelloWorld HTTP/1.1\r\n";//必须
os.write(protocol.getBytes());
String body = "key1=value1&key2=value2";
String head = "Host: " + IP + ":" + PORT + "\r\n" +//必须
"Content-Length: " + body.length() + "\r\n" +//必须
"Content-Type: application/x-www-form-urlencoded\r\n" +
"Connection: Keep-Alive\r\n" +
"User-Agent: myhttp/3\r\n" +
"Cookie: key=value; key2=value2\r\n" +
"head1: Value1\r\n" +
"head2: Value2\r\n";
os.write(head.getBytes());
os.write("\r\n".getBytes());
os.write(body.getBytes());
os.flush();
InputStream is = socket.getInputStream();
String resp = readInputStream(is);
System.out.println("" + resp);
is.close();
socket.close();
}
可以看到,除请求行与host必须外,如果body里有数据,Content-length也是必须的,长度就是body数据长。然后cookie也是放在head中的,以; 分隔,key=value形式。在write完head头 后,再写一个换行符,然后写body。返回结果如下:
HTTP/1.1 200
respHead: v1
Set-Cookie: cookie1=value1
Content-Type: text/html;charset=utf-8
Content-Length: 362
Date: Tue, 23 Jul 2019 17:00:48 GMT
protocol :HTTP/1.1
your cookie start-------
key=value
key2=value2
cookie end-------
your header start-------
host:192.168.31.236:8080
content-length:23
content-type:application/x-www-form-urlencoded
connection:Keep-Alive
user-agent:myhttp/3
cookie:key=value; key2=value2
head1:Value1
head2:Value2
your header end-------
query:null
body:key1=value1&key2=value2
对应于服务器post方法,新增了一个cookie 与 respHead给客户端,空行之后全是body。至此,我们自己手动通过socket实现一个http就算完成了,主要是head与body中的换行与规则要清楚,可以自己试试。这部分可以说是okhttp最核心的部分,如果你能看懂上面这些,并有大致理解,okhttp你就看懂了一半,因为okhttp也是通过socket建立连接,然后发送相应规则的数据,然后解析成相应的对象供上层去处理。上一篇文章也说了,只要你能建立一个Tcp连接,然后发送相应格式的数据,就是发起一个http请求,如Telnet工具,这个工具可以建立一个tcp连接。
wzhdeMacBook-Pro:shell wzh$ telnet 10.10.13.201 8080
Trying 10.10.13.201...
Connected to 10.10.13.201.
Escape character is '^]'.
^]
telnet>
POST /WebServerTest_war_exploded/HelloWorld HTTP/1.1
head1: headValue1
head2: headValue2
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
Host: 10.10.14.235:8080
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.10.0
key1=value1&key2=value2
HTTP/1.1 200
respHead: v1
Set-Cookie: cookie1=value1
Content-Type: text/html;charset=utf-8
Content-Length: 342
Date: Mon, 15 Jul 2019 03:42:28 GMT
protocol :HTTP/1.1
your cookie start-------
cookie end-------
your header start-------
head1:headValue1
head2:headValue2
content-type:application/x-www-form-urlencoded
content-length:23
host:10.10.14.235:8080
connection:Keep-Alive
accept-encoding:gzip
user-agent:okhttp/3.10.0
your header end-------
query:null
body:key1=value1&key2=value2
Telnet 工具的详细用法请参考其它文章。
Okio 用法
okio是由square公司开发的用于IO读取。补充了Java.io和java.nio的不足,以便能够更加方便,快速的访问、存储和高效处理数据。内部的读写操作是在内存中进行的。github地址主要方法
其中Source对应于输入流,Sink对应于输出流,下为主要类对应关系
Sink --> OutPutStream 输出流
Source --> InputStream 输入流
BufferedSink --> BufferedOutputStream 缓存输出流
BufferedSource --> BufferedInputStream 缓存输入流
每个source与sink的构造都可以通过File、Socket、InputStream来创建,用法差不多,下面为例子
public class OkIoTest {
public static void main(String[] args) {
try {
Source fileSource = Okio.source(new File("/Users/xx.tx"));
BufferedSource buffSource = Okio.buffer(fileSource);
File f2 = new File("/Users/newFile.txt");
BufferedSink bufferSink = Okio.buffer(Okio.sink(f2));
//文件复制
String line;
while ((line=buffSource.readUtf8Line())!=null) {
System.out.println(line);
bufferSink.writeUtf8(line);
}
buffSource.close();
bufferSink.close();
//保存对象到文件
bufferSink = Okio.buffer(Okio.sink(f2));
Person p = new Person(10, "aa");
bufferSink.writeString(serialize(p).base64(), Charset.defaultCharset());
bufferSink.close();
//从文件读取对象
String string=Okio.buffer(Okio.source(f2)).readString(Charset.defaultCharset());
ByteString byteString = ByteString.decodeBase64(string);
Buffer b = new Buffer();
b.write(byteString);
Person p2 = (Person) new ObjectInputStream(b.inputStream()).readObject();
System.out.println("p2=" + p2);
} catch (Exception e) {
e.printStackTrace();
}
}
private static ByteString serialize(Object o) throws IOException {
Buffer buffer = new Buffer();
try (ObjectOutputStream objectOut = new ObjectOutputStream(buffer.outputStream())) {
objectOut.writeObject(o);
}
return buffer.readByteString();
}
}
展示了从一个文件输入到别一个文件输入的例子,和对象的存取,用法还是比较简单的。大家只需要记住对应关系类就可以了。下面展示用okio发送post请求
private static void httpPostOkioTest() throws IOException {
Socket socket = new Socket(IP, PORT);
socket.setKeepAlive(true);
BufferedSink sink = Okio.buffer(Okio.sink(socket.getOutputStream()));
String protocol = "POST /WebServerTest_war_exploded/HelloWorld HTTP/1.1\r\n";//必须
sink.writeUtf8(protocol);
String body = "key1=value1&key2=value2";
String head = "Host: " + IP + ":" + PORT + "\r\n" +//必须
"Content-Length: " + body.length() + "\r\n" +//必须
"Content-Type: application/x-www-form-urlencoded\r\n" +
"Connection: Keep-Alive\r\n" +
"Accept-Encoding: gzip\r\n" +
"User-Agent: myhttp/3\r\n" +
"Cookie: key=value; key2=value2\r\n" +
"head1: Value1\r\n" +
"head2: Value2\r\n";
sink.writeUtf8(head);
sink.writeUtf8("\r\n");
sink.writeUtf8(body);
sink.flush();
BufferedSource is = Okio.buffer(Okio.source(socket.getInputStream()));
String resp = is.readByteString().utf8();
System.out.println(resp);
socket.close();
}
输出和httpPostTest 是一样的。