最近看了一下http协议,看起来没啥难度,于是用java写了一个http服务。接下来可以用C++写一个。
最麻烦的就是解析http协议,http大致分为3种。GET,POST (不带文件),POST,带文件,首先来挨个看一下,看http协议数据比较简单,启动一个tcp监听(这里使用8080),然后等待数据到来就行,数据到了,输出一下。代码比较简单。掠过。
一:GET请求输出
首先用postman 请求 http://127.0.0.1:8080/index
java监听到的数据:
可以看出, 第一行 请求:方法(GET/POST)(空格) 地址 (空格) 协议(固定的)
剩余的比较简单,都可以看作header,按行读取,然后根据冒号字符串分割,存储到header就行
二:POST(不带文件),不带文件就是urlencodedd,实际上就是类似get请求的字符串
postman使用post请求 ,带了3个表单参数
java监听到的请求数据:
跟get相比,form表单的数据,放到了最后,格式跟get一样。
http协议规定,http头和body之间有个空行,因为可以用按行读取的方式,读取到空行,就认为头结束,数据body开始
三:POST(带文件),这个是相对解析起来比较困难的,多个form数据使用分界符分开
,使用post发送请求,带文件,下图红框所示。
获取的请求数据
可以看到,在Content-Type有数据类型 multipart/form-data,和界定boundary
对于普通字段的格式,如上图,一行界定,一行表单名称,空行,文字内容
对于文件,格式见上图,比普通表单字符增加了一行文件类型变成了
一行界定,一行文件类型,空行,接下来是文件内容,文件内容后面增加\r\n
四:POST ,raw请求,就是目前常用的,在请求里面发送json的方式
监听到的数据如下从图中看来,跟不带文件的post请求差不多,后面测试了一下,实际上是可以发送任何格式的字符串,urlencoded也可以。
http协议解析差不多就这样,接下来就是实现方法。
首先使用SocketServer监听一个端口,并且启动一个永久线程,这个线程用来等待客户端链接,大致代码:
一旦有客户端链接进来,把请求放到线程池里面去运行,每次请求,启动一个线程,处理完毕,线程接收。因此使用线程池比较合适,代码如下:
接下来就是处理http请求核心逻辑。大致的处理步骤为
1:接收请求的所有数据到缓冲区,缓冲区大小决定了post带文件时候,最大字节数。很多系统可以设置。
2:按行读取http头,直到遇到空行,表示头读取结束,(空行是请求头和请求体的分界),头数据读取完,需要解析一下,分析是否有文件。
3:GET请求,处理完毕,不需要做特殊处理
4:如果是POST请求,看一下是否具有文件,如果没有文件,直接从空行后面,读取全部数据,就是请求体。存储到字符串body。
如果body里面存在a=1&b=2这样的表达式,使用正则拆分一下,存储到请求参数里面
5:如果是post带文件,就比较复杂,换成伪代码:
while(数据没有读取完毕)
读取一行,这一行是界定符,直接跳过
读取一行,这一行是表单名称
如果 表单名称,含有filename字符,那么这个就是一个文件表单
读取一行,这一行是文件类型
读取一个空格,不处理,空行后面是文件开始
根据界定符,查找下一个界定符的位置,下一个界定符开始位置,往前推2个字符,就是文件结尾。因为文件结尾有\r\n
找到文件开始和结束位置后,把文件内容提取出来,就是文件二进制内容。可以直接保存
如果 表单名称,不含有filename字段,那么就是一个普通表单字段
读取一个空行,不处理
读取一个有内容的行,就是值
最后就是解析http请求的核心代码
package com.dajia.server.service;
import java.io.InputStream;
import java.net.Socket;
import com.dajia.server.model.File;
import com.dajia.server.model.Request;
/***
* http协议解析
* 在线程池种运行,所以实现Runnable
* @author qujia
*
*/
public class RequestHandler implements Runnable {
Socket c;
static int maxbuf=1024*1024*10;
byte[] buf;
int pos;
int p1;
public RequestHandler(Socket s)
{
System.out.println("request from "+s.getRemoteSocketAddress());
c=s;
}
public void run() {
try {
buf=new byte[maxbuf];//创建一个请求缓冲区
int len=0;//已经读取的字节长度
InputStream is=c.getInputStream(); //获取socket的输入流
while(is.available()>0) {
len+=is.read(buf,len,maxbuf-len); //读取到缓存区
}
//System.out.println("--------------------request data---------------------");
//System.out.println(new String(buf,0,len));
//System.out.println("-----------------------------------------------------");
//---------------------------请求数据接受完毕-----------------------------
Request request=new Request(c);//创建一个请求
String s=readLine();//第一行
request.handleMethod(s);//处理第一行
while(s.length()>0) {//到第一个空行就结束了
request.handNextLine(s);//其他行
s=readLine();//读取下一行
}
request.initRequest();//处理请求头数据,判断请求类型,等操作
//----------------------------请求头处理完毕---------------------------------
if(request.isFormData()) {//如果是带文件的
//如果是有文件数据
while(pos<buf.length) {
s=readLine();//分割行,doundary 行
s=readLine();//内容,表单字段名称
if(s==null || s.length()==0)break;
if(s.indexOf("file")==-1) {
//普通表单
readLine();//空行,每次和内容中间有个\r\n
String v=readLine();//内容
request.addFormItem(s, v);//添加到request
}
else {
//文件表单
File f=new File();//新建一个
f.setName(s);//这个是表单文件域的名字和文件名
f.setContentType(readLine());//这个是文件类型
s=readLine();//读取一个空行,文件内容开始之前有\r\n
int p2=findData(request.getBoundary().getBytes(), pos)-2;//查找下一个边界,边界之前有\r\n
f.setData(buf, pos, p2);//文件内容复制
pos=p2+2;//跳过两个字符\r\n
request.addFile(f);//添加到请求数据里面
}
}
}
else {
//如果没有文件,那么body就是a=1&b=2&c=123这种拼接的字符串
s=readLeft();//剩余数据
request.setFromData(s); //解析表单数据
}
//---------------------request 请求 解析完毕 ---------------------------
//System.out.println(request.toString()); //输出解析结果
//模拟返回一个html文件
request.getResponse().setBody("<!DOCTYPE html>\r\n" +
"<html>\r\n" +
" <head>\r\n" +
" <title>sadasasdsd</title>\r\n" +
" </head>\r\n" +
" <body>\r\n" +
" wqweqwe\r\n" +
" </body>\r\n" +
"</html>");
request.getResponse().doResponse(); //返回
Thread.sleep(200);
c.close();//关闭链接
}
catch (Exception e) {
// TODO: handle exceptio
e.printStackTrace();
}
}
/**
* 读取一行
* @return
*/
private String readLine()
{
int p=findChar((byte)0x0a, pos);//寻找\n
if(p==-1)return null;
String s= new String(buf,pos,p-pos);//截取字符串
pos=p+1;
return s.trim();
}
/**
* 读取剩余所有
* @return
*/
private String readLeft() {
String s=new String(buf,pos,buf.length-pos).trim();
return s;
}
/**
* 在字节缓冲区查找一个字符
* @param c
* @param start
* @return
*/
private int findChar(byte c,int start) {
for(int i=start;i<buf.length;i++) {
if(buf[i]==c) {
return i;
}
}
return -1;//没有找到
}
/**
* 在字节数组里面查找另外一个数组
* 类似字符串的indexOf
* @param d
* @param start
* @return
*/
private int findData(byte[] d,int start) {
for(int i=start;i<buf.length;i++) {
if(buf[i]==d[0]) {//找到第一个字节了
int t=1;
for(;t<d.length;t++) {//比较后面的字节
if(d[t]!=buf[i+t]) {
t=-1;//如果不一样
break;
}
}
if(t!=-1) {//不等于-1,就是匹配成功
return i;
}
}
}
return -1;//没有找到
}
}