使用java手写一个http服务,Http协议解析

最近看了一下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;//没有找到
			
		}
	
	

	
	
}

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值