HTTP实际上是基于TCP的应用层协议,它在更高的层次封装了TCP的使用细节,是网络请求操作更为易用. TCP连接是因特网上基于流的可靠连接,它为HTTP提供了一条可靠的比特传输管道. 从TCP连接一端填入的字节会从另一端以原有的顺序,正确地传递出来,如下图所示.
Client客户端Client客户端Web服务端Web服务端数据在网络中传输
TCP的数据是通过名为IP分组(或IP数据报)的小数据块来发送的. 这样的话,如下图的HTTP协议所示,HTTP就是”HTTP over TCP over IP”这个”协议栈”中的最顶层了.
HTTP要传送一条报文时,会以流的形式将报文数据的内容通过一条打开的TCP连接按序传输. TCP收到数据流之后,会将数据流分割成被称作段的小数据块,并将段封装在IP分组中,通过因特网进行传输. 所有这些工作都是由TCP/IP软件来处理的,程序员什么都看不到.
下面我们就模拟一个简单的Web服务器来深度了解一下HTTP的报文格式以及HTTP协议与TCP协议之间的协作原理.
一个HTTP请求就是一个典型的C/S模式,服务端在监听某个端口,客户端向服务端的端口发起请求. 服务端解析请求,并且向客户端返回结果. 下面我们就先看看这个简单的Web服务端.
代码如下:public class SimpleHttpServer extends Thread {
public static void main(String[] args) {
new SimpleHttpServer().start();
}
// 服务端Socket
ServerSocket mSocket = null;
public SimpleHttpServer() {
try {
mSocket = new ServerSocket(SocketTool.PORT);
} catch (IOException e) {
e.printStackTrace();
}
if (mSocket == null) {
throw new RuntimeException("服务器Socket初始化失败");
}
}
@Override
public void run() {
try {
while (true) {
// 无限循环,进入等待连接状态
System.out.println("等待连接中");
// 一旦接收到连接请求,构建一个线程来处理
new DeliverThread(mSocket.accept()).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
SimpleHttpServer继承自Thread类,在构造函数中我们会创建一个监听10086端口的服务端Socket,并且覆写Thread的run函数,在该函数中开启无限循环,在该循环中调用ServerSocket的accept()函数等待客户端的连接,该函数会阻塞,知道有客户端进行连接,接收连接之后会构造一个线程来处理该请求. 也就是说,SimpleHttpServer本身是一个子线程,它在后台等待客户端的连接,一旦接收到连接又会创建一个线程处理该请求,避免阻塞SimpleHttpServer线程.
现在我们一步一步来分析连接处理线程DeliverThread的代码:static class DeliverThread extends Thread {
Socket mClientSocket;
// 输入流
BufferedReader mInputStream;
// 输出流
PrintStream mOutputStream;
// 请求方法,GET、POST等
String httpMethod;
// 子路径
String subPath;
// 分隔符
String boundary;
// 请求参数
Map<String, String> mParams = new HashMap<String, String>();
// 请求headers
Map<String, String> mHeaders = new HashMap<String, String>();
// 是否已经解析完Header
boolean isParseHeader = false;
public DeliverThread(Socket socket) {
mClientSocket = socket;
}
@Override
public void run() {
try {
// 获取输入流
mInputStream = new BufferedReader(new InputStreamReader(
mClientSocket.getInputStream()));
// 获取输出流
mOutputStream = new PrintStream(mClientSocket.getOutputStream());
// 解析请求
parseRequest();
// 返回Response
handleResponse();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭流和Socket
IoUtils.closeQuickly(mInputStream);
IoUtils.closeQuickly(mOutputStream);
IoUtils.closeSocket(mClientSocket);
}
}
//代码省略
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
DeliverThread也继承自Thread,在run函数中主要封装了如下步骤:
- 获取客户端Socket的输入,输出流用于读写数据;
- 解析请求参数;
- 处理,返回请求结果;
- 关闭输入,输出流,客户端Socket.
上文我们说过TCP的数据操作是基于流的,因此得到客户端Socket连接之后,我们首先获取到它的输入,输出流. 其中我们可以从输入流中获取该请求的数据,而通过输出流就可以将结果返回给该客户端. 得到流之后我们首先解析该请求,根据它请求的路径,header,参数等作出处理,最后将处理结果通过输出流返回给客户端. 最终关闭流和Socket.
在分析HTTP请求解析的代码之前,我们再来回顾一下HTTP请求的报文格式,如下图所示
下面我们看一下解析请求的具体实现,即parseRequest函数:private void parseRequest() {
String line;
try {
int lineNum = 0;
// 从输入流读取客户端发送过来的数据
while ((line = mInputStream.readLine()) != null) {
//第一行为请求行
if (lineNum == 0) {
parseRequestLine(line);
}
// 判断是否是数据的结束行
if (isEnd(line)) {
break;
}
// 解析header参数
if (lineNum != 0 && !isParseHeader) {
parseHeaders(line);
}
// 解析请求参数
if (isParseHeader) {
parseRequestParams(line);
}
lineNum++;
}
} catch (IOException e) {
e.printStackTrace();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
在parseRequest函数中,我们按照数据的分步进行解析. 首先解析第一行的请求行数据,即当lineNum为0时调用parseRequestLine函数进行解析. 该函数的实现如下:// 解析请求行
private void parseRequestLine(String lineOne) {
String[] tempStrings = lineOne.split(" ");
httpMethod = tempStrings[0];
subPath = tempStrings[1];
System.out.println("请求行,请求方式 : " + tempStrings[0] + ", 子路径 : " + tempStrings[1]
+ ",HTTP版本 : " + tempStrings[2]);
System.out.println();
}
在上文的格式分析中我么你说过,请求行由3部分组成,即请求方式,请求子路径,协议版本,它们之间通过空格来进行分割. 因此,在parseRequestLine中我们用空格分隔请求行字符串,得到的结果就是这3个值.
请求行后面紧跟着请求Header,因此,我们的下一步就是解析Header区域. 对应的函数为parseHeaders,代码如下:// 解析header,参数为每个header的字符串
private void parseHeaders(String headerLine) {
// header区域的结束符
if (headerLine.equals("")) {
isParseHeader = true;
System.out.println("-----------> header解析完成\n");
return;
} else if (headerLine.contains("boundary")) {
boundary = parseSecondField(headerLine);
System.out.println("分隔符 : " + boundary);
} else {
// 解析普通header参数
parseHeaderParam(headerLine);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
每个header为一个独立行,格式为参数名: 参数值,还有一种情况是参数名1: 参数值2;参数名2: 参数值2. 例如下面两个header:Content-Length: 1234
Content-Type: multipart/form-data; boundary=OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp
第一个header参数名为Content-Length,值为1234. 第二个header在同一行内有两个数据,分别为值为multipart/form-data的Content-Type,以及值为OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp的boundary. header与请求参数之间通过一个空行分隔,因此,我们检测到header数据为空时则认为是header参数的结束行.
当一个header行数据中含有boundary字段时,则调用parseSecondField函数解析,该函数实现如下:// 解析header中的第二个参数
private String parseSecondField(String line) {
String[] headerArray = line.split(";");
parseHeaderParam(headerArray[0]);
if (headerArray.length > 1) {
return headerArray[1].split("=")[1];
}
return "";
}
因为boundary参数在header格式的第二个参数的位置上,因此,这里通过分号进行分割,获取数组第二个位置的数据,也就是boundary=OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp,然后再进行解析.
普通的header则是参数名: 参数值的格式,我们通过parseHeaderParam函数进行解析,代码如下:// 解析单个header
private void parseHeaderParam(String headerLine) {
String[] keyvalue = headerLine.split(":");
mHeaders.put(keyvalue[0].trim(), keyvalue[1].trim());
System.out.println("header参数名 : " + keyvalue[0].trim() + ", 参数值 : "
+ keyvalue[1].trim());
}
解析完header之后我们就开始解析请求参数了. 对于POST和PUT请求来说,它们的每个参数格式都是固定的,格式如下:--boundary值
header-1: value-1
...
header-n: value-n
空行
参数值
由于在我们的例子中每个请求参数只有一个header字段,因此, 我们的每个参数的格式简化为:--boundary
Content-Disposition: form-data; name="参数名"
空行
参数值
根据上述格式,我们再来看解析函数:// 解析请求参数
private void parseRequestParams(String paramLine) throws IOException {
if (paramLine.equals("--" + boundary)) {
// 读取Content-Disposition行
String ContentDisposition = mInputStream.readLine();
// 解析参数名
String paramName = parseSecondField(ContentDisposition);
// 读取参数header与参数值之间的空行
mInputStream.readLine();
// 读取参数值
String paramValue = mInputStream.readLine();
mParams.put(paramName, paramValue);
System.out.println("参数名 : " + paramName + ", 参数值 : " + paramValue);
}
}
// 是否是结束行
private boolean isEnd(String line) {
return line.equals("--" + boundary + "--");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
至此,整个请求的各个部分均已解析完成. 后面要做的就是根据用户的请求返回结果. 在这里我们直接返回了一个固定的Response. 代码如下:// 返回结果
private void handleResponse() {
//模拟处理耗时
sleep();
//向输出流写数据
mOutputStream.println("HTTP/1.1 200 OK");
mOutputStream.println("Content-Type: application/json");
mOutputStream.println();
mOutputStream.println("{"stCode":"success"}");
}
private void sleep() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
在handleResponse函数中,通过Socket的输出流向客户端写入数据. 写入的数据也遵循了响应报文的基本格式,格式如下:响应行
header区域
空行
响应数据
向客户端写完数据后,我们就会关闭输入,输出流以及Socket,至此,整个请求,响应流程完毕.
服务端逻辑分析完成之后我们再来看看客户端的实现. 从上述的分析以及平时的开发经验我们知道,客户端要做的就是主动向服务器发起HTTP请求,它们之间的通信通道就是TCP/IP,因此,也是基于Socket实现. 下面我们就模拟一个Http POST请求,代码如下:public class HttpPost {
public String url;
// 请求参数
private Map<String, String> mParamsMap = new HashMap<String, String>();
private static final int PORT = 10086;
//客户端Socket
Socket mSocket;
public HttpPost(String url) {
this.url = url;
}
public void addParam(String key, String value) {
mParamsMap.put(key, value);
}
public void execute() {
try {
// 创建Socket连接
mSocket = new Socket(this.url, PORT);
PrintStream outputStream = new PrintStream(mSocket.getOutputStream());
BufferedReader inputStream = new BufferedReader(new InputStreamReader(
mSocket.getInputStream()));
final String boundary = "my_boundary_123";
// 写入header
writeHeader(boundary, outputStream);
// 写入参数
writeParams(boundary, outputStream);
// 等待返回数据
waitResponse(inputStream);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (mSocket != null) {
try {
mSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/代码省略
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
HttpPost构造函数中传入请求的URL地址,然后用户可以调用addParam函数添加普通的文本参数,当用户设置好参数之后就可以通过execute函数执行该请求. 在execute函数中客户端首先创建Socket连接,目标地址就是用户执行的URL以及端口. 连接成功之后客户端就可以获取到输入,输出流,通过输出流客户端可以向服务端发送数据,通过输入流则可以获取服务端返回的数据. 之后我们一次写入header,请求参数,最后等待Response的返回.
在该示例中,我们将header固定作出如下设置,代码如下:private void writeHeader(String boundary, PrintStream outputStream) {
outputStream.println("POST /api/login/ HTTP/1.1");
outputStream.println("content-length:123");
outputStream.println("Host:" + this.url + ":" + PORT);
outputStream.println("Content-Type: multipart/form-data; boundary=" + boundary);
outputStream.println("User-Agent:android");
outputStream.println();
}
然后,我们将mParamsMap中的所有参数通过输出流传递给服务端,代码如下:private void writeParams(String boundary, PrintStream outputStream) {
Iterator<String> paramsKeySet = mParamsMap.keySet().iterator();
while (paramsKeySet.hasNext()) {
String paramName = paramsKeySet.next();
outputStream.println("--" + boundary);
outputStream.println("Content-Disposition: form-data; name=" + paramName);
outputStream.println();
outputStream.println(mParamsMap.get(paramName));
}
// 结束符
outputStream.println("--" + boundary + "--");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
每个参数都必须遵循特定的格式,在上文服务器解析参数时就是按照这里设定的格式进行. 格式如下:--boundary
Content-Disposition: form-data; name="参数名"
空行
参数值
当参数结束之后需要写一个结束行,格式为:两个斜杠加上boundary值再加上两个斜杠. 此时请求数据就已经发送到服务端,此时我们等待服务器返回数据. 得到返回的数据之后将结果输出到控制台. 代码如下:private void waitResponse(BufferedReader inputStream) throws IOException {
System.out.println("请求结果: ");
String responseLine = inputStream.readLine();
while (responseLine == null || !responseLine.contains("HTTP")) {
responseLine = inputStream.readLine();
}
//输出Response
while ((responseLine = inputStream.readLine()) != null) {
System.out.println(responseLine);
}
}
此时,客户端的流程也执行完毕.
下面,运行这个例子. 首先需要启动服务器,代码如下:public static void main(String[] args) throws Exception {
new SimpleHttpServer().start();
}
服务器启动之后就会在后台等待客户端发起连接,此时我们再启动客户端,设置参数之后执行一个Http POST请求:HttpPost httpPost = new HttpPost("127.0.0.1");
// 设置两个参数
httpPost.addParam("username", "mr.simple");
httpPost.addParam("pwd", "my_pwd123");
// 执行请求
httpPost.execute();
执行结果如下图所示:
- 服务端接到请求
- 客户端请求结果
本文中我们用一个简单的示例模拟了Web服务器与客户端你的交互过程. 整个示例就是在TCP智商封装了一层HTTP,用户通过HTTP相关的类进行操作,但是传输层依旧是通过TCP层. 客户端与服务端之间开辟了一条双向的Socket,通过输入,输出流向对方发送,获取数据,而双方都遵循了规定的HTTP协议,因此,数据的发送与解析都能够顺利进行. 通过HTTP层屏蔽了直接使用Socket的复杂细节,使得整个通信过程更加简单,易用.
完整示例:
SocketSamples
|