android端实现http服务器,具备文件上传等功能,纯JAVA实现,无依赖包

最近需要在我们的安卓设备上实现通过网页访问设备,进行相关配置、上传数据等操作,因此就需要在安卓端实现一个http服务器。(其实代码也可以用于PC端,只不过PC端已经有太多成熟的框架了,JDK7/8之后貌似就内置了一个轻量的HTTP服务器)。

采用java socket实现的http服务器网上有较多的例子,但是例子大部分都比较简单,不具备文件上传的功能,于是结合网上的列子动手写了个具备文件上传、请求资源文件、处理请求的简单的http服务器,需要的朋友可以参考下:
这里写图片描述

  • 创建socket 监听,客户端浏览器在打开地址后,就会向服务器建立一个tcp连接(http协议是基于TCP协议封装的)
  • 服务器获取客户端的输入流并解析头部,也就是说需要解析浏览器封装的HTTP协议信息
  • 根据解析的头部信息判断客户端请求的资源类型,并进行相应的处理

处理http请求的部分,这里不再详述,大家也可直接参考代码,重点说下文件上传部分:
1、根据Content-Type来判断当前是否为文件上传请求,即:
this.contentType.startsWith(“multipart/form-data”);表示当前为上传文件请求
2、接下来就是根据RFC协议去解析请求体中的上传文件数据了, RFC协议中规定的http上传文件文件格式如下:
这里写图片描述
其中的--------------------------------1878979834就是“边界值”,是文件数据的开始、结束标志。
一开始也就是在这里遇到的难处,不知道是不是有的人也跟我一开始的做法类似,使用bufferedReader去按行解析,最后却发现提取出来的数据与实际的文件不一致、不完整。并不是编码格式的原因。原因应该是bufferedReader默认会提前缓存了一部分数据,导致最终按行读取获取到的数据不完整(距离问题解决过去了很久,具体原因是不是这个原因记得不是太清)。
总之最后发现在如果文件上传的话就不能采用bufferdReader去按行读取。既然这样那就得自己去按字节解析,也就是说自己去实现readLine,可以看到,我实现了两个readline方法,第一个读取一行后返回改行的字符串,用于解析http请求的头部。第二个将一行的数据(包括换行符)读取到byte[]用于提取上传的文件。以及一个快速比较byte数组的方法,用于快速判断当前是否读取到了文件的边界,代码如下:

	/**
     * 读取一行
     * @return 
     */
    private String readLine() {
        try {
            byte[] buffer = new byte[1024];//默认缓存1024
            int b = 0, pos = 0;
            while ((b = mStream.read()) >= 0) {
                buffer[pos] = (byte) b;
                if (pos > 0 && buffer[pos] == 0x0A && buffer[pos - 1] == 0x0D) {//读取到换行符0d 0a或13 10 表示换行
                    return new String(buffer, 0, pos - 1);
                }
                pos++;//当前坐标+1
                if (pos == buffer.length) {//缓冲区已满则扩充缓存区
                    byte[] old = buffer;
                    buffer = new byte[old.length + 1024];//每次扩充1024
                    System.arraycopy(old, 0, buffer, 0, old.length);
                }
            }
            return new String(buffer, 0, pos);
        } catch (Exception ex) {
            return null;
        }
    }
    /**
     * 读取一行byte,返回读取的长度
     * @param buffer
     * @return
     */
    private int readLine(byte[] buffer) {
    	int b = 0, pos = 0;
        try {
            while (pos<buffer.length&&(b = mStream.read()) !=-1) {
                buffer[pos] = (byte) b;
                if (pos > 0 && buffer[pos] == 0x0A && buffer[pos - 1] == 0x0D) {//读取到换行符0d 0a或13 10 表示换行
                    return pos+1;//返回读取的长度
                }
                pos++;//当前坐标+1
            }
        } catch (Exception ex) {
        }
        return pos;
    }
    /**
	 * 比较byte数组
	 * 
	 * @param src
	 * @param des
	 * @return
	 */
	private boolean startsWith(byte[] src, byte[] des) {
		if (src.length < des.length) {
			return false;
		}
		for (int i = 0; i < des.length; i++) {
			if (src[i] != des[i]) {
				return false;
			}
		}
		return true;
	}

其实关键就是需自己去实现readline方法,下面是获取客户端输入流后,解析输入流的过程代码:

public MyRequest(InputStream in) {
        mStream = new BufferedInputStream(in);
        try {
            //读取第一行, 请求地址
            String line = this.readLine();
            if (line == null) {
                return;
            }
            // 获得请求的资源的地址
            String resource = line.substring(line.indexOf("/"), line.lastIndexOf("/") - 5);
            this.requestUrl = URLDecoder.decode(resource, "UTF-8");// 反编码 URL地址
            this.method = new StringTokenizer(line).nextElement().toString();// 获取请求方法, GET或者POST
            // 读取所有浏览器发送过来的请求参数头部信息
            int contentLen = 0;// 如果为POST方法,则会有消息体长度
            while ((line = this.readLine()) != null) {
            	Log.d(TAG, line);
                if (line.startsWith("Content-Length")) {
                    contentLen = Integer.parseInt(line.split(":")[1].trim());
                }
                if (line.startsWith("Content-Type")) {
                    this.contentType = line.split(":")[1].trim();
                }
                if (line.equals("")) {// 空行表示头结束
                    break;
                }
            }
            if ("POST".equalsIgnoreCase(this.method) && contentLen > 0) {// 显示 POST表单提交的内容, 这个内容位于请求的主体部分
                try {
                    if (this.contentType != null && this.contentType.startsWith("multipart/form-data")) {// 上传文件请求
                        this.filePath=this.extractRFCFile();
                    } else {
                        byte[] buffer = new byte[contentLen];
                        mStream.read(buffer, 0, buffer.length);// 不能调用readline,否则会导致阻塞
                        String postTextBody = new String(buffer);
                        this.params = parseQueryParam(postTextBody);
                    }
                } catch (Exception ex) {
                	logger.error("读取POST数据出错", ex);
                }
            }
            int queryIndex = this.requestUrl.indexOf("?");
            if (queryIndex > 0) {
            	this.url=this.requestUrl.substring(0,queryIndex);
                if (this.params == null) {
                    this.params = new HashMap<String, String>();
                }
                this.params.putAll(parseQueryParam(this.requestUrl.substring(queryIndex + 1)));
            }else{
            	this.url=this.requestUrl;
            }
        } catch (Exception ex) {
        	logger.error("解析http请求为request出错", ex);
        }
    }

下面是提取上传文件并写到本地临时文件的代码:

/**
     * 提取http上传的文件
     * @return 
     */
    private String extractRFCFile() {
    	byte[] boundary = this.readLine().getBytes();// 根据RFC协议第一行是边界
        Log.d(TAG, "上传数据,边界为:" + new String(boundary));
        String line;
        while ((line = this.readLine()) != null) {
        	Log.d(TAG, line);
            if (line.startsWith("Content-Disposition")) {
            	//获取上传的文件名
            }
            if (line.equals("")) {// 空行表示头结束
                break;
            }
        }
        String savePath=Confing.CLIENT_BASEPATH+"upload.tmp";
        try {
            FileOutputStream fos = new FileOutputStream(savePath);
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = this.readLine(buffer)) > 0) {
                if (this.startsWith(buffer, boundary)) {
                	Log.d(TAG, "文件读取结束");
                    break;
                }
                fos.write(buffer, 0, count);//会导致多一个换行符
            }
            fos.flush();
            fos.close();
        } catch (Exception ex) {
        	logger.error("提取HTTP上传的文件出错", ex);
            return null;
        }
        return savePath;
    }

下面是接受一个客户端socket接入后的处理部分代码:

static class HttpWorkHandle extends Thread {
		private Socket client;

		public HttpWorkHandle(Socket socket) {
			this.client = socket;
		}

		public void run() {
			try {
				if (client != null) {
					logger.info("连接到服务器的用户:" + client);
					InputStream ins = null;
					OutputStream out = null;
					try {
						ins = client.getInputStream();
						out = client.getOutputStream();
						MyRequest request = new MyRequest(ins);
						if (request.method == null) {
							return;
						}
						MyResponse response = new MyResponse(out);
						//根据contentType判断是不是业务类请求
						if (request.contentType.startsWith("application/x-www-form-urlencoded") 
								|| request.contentType.startsWith("application/json")
								|| request.contentType.startsWith("multipart/form-data")
								|| request.url.endsWith(".do")) {
							PrintWriter pw = new PrintWriter(out, true);
							String responseText = new WebDispatcher(mContext).doRespond(request, response);
							pw.println("HTTP/1.0 200 OK");// 返回应答消息,并结束应答
							pw.println("Content-Type:text/plain;charset=UTF-8");
							pw.println();// 根据 HTTP 协议, 空行将结束头信息
							if (responseText != null) {
								pw.print(responseText);
							}
							pw.close();
						} else {// 请求资源文件
							try {
								String resource;
								if (request.requestUrl.equals("/") || request.requestUrl.endsWith(SERVNAME)) {
									resource = "web/index.html";
								} else {
									resource = "web" + request.requestUrl.substring(request.requestUrl.indexOf(SERVNAME) + SERVNAME.length());
								}
								InputStream stream = null;
								try {
									stream = mContext.getAssets().open(resource);
								} catch (Exception ex) {
									Log.d("HTTPServer", "资源文件不存在:" + resource);
								}
								if (stream != null) {
									byte[] buffer = new byte[1024 * 4];
									int len;
									while ((len = stream.read(buffer)) != -1) {
										out.write(buffer, 0, len);
									}
									out.flush();
									stream.close();
								} else {
									out.write(this.errorMessage().getBytes());// 返回失败信息
								}
							} catch (Exception ex) {
								logger.error("客户端请求资源文件出错", ex);
							}
						}
					} catch (Exception ex) {
						logger.error("HTTP服务器错误", ex);
					} finally {
						if (ins != null) {
							ins.close();
						}
						if (out != null) {
							out.close();
						}
						client.close();
					}
				}
			} catch (Exception ex) {
				logger.error("处理客户端请求出错", ex);
			}
		}

以下是别人总结的Http协议相关的东西,结合这个基本就可以理解在解析http头部时的一些步骤了:
** HTTP请求包括的内容
客户端连上服务器后,向服务器请求某个web资源,称之为客户端向服务器发送了一个HTTP请求。
一个完整的HTTP请求包括如下内容:一个请求行、若干消息头、以及实体内容,范例:
  
 HTTP请求的细节——请求行
  请求行中的GET称之为请求方式,请求方式有:POST、GET、HEAD、OPTIONS、DELETE、TRACE、PUT,常用的有: GET、 POST
  用户如果没有设置,默认情况下浏览器向服务器发送的都是get请求,例如在浏览器直接输地址访问,点超链接访问等都是get,用户如想把请求方式改为post,可通过更改表单的提交方式实现。
  不管POST或GET,都用于向服务器请求某个WEB资源,这两种方式的区别主要表现在数据传递上:如果请求方式为GET方式,则可以在请求的URL地址后以?的形式带上交给服务器的数据,多个数据之间以&进行分隔,例如:GET /mail/1.html?name=abc&password=xyz HTTP/1.1
  GET方式的特点:在URL地址后附带的参数是有限制的,其数据容量通常不能超过1K。
  如果请求方式为POST方式,则可以在请求的实体内容中向服务器发送数据,Post方式的特点:传送的数据量无限制。
 HTTP请求的细节——消息头
  HTTP请求中的常用消息头
  Accept代表发送端(客户端)希望接受的数据类型
Content-Type代表发送端(客户端|服务器)发送的实体数据的数据类型
  Accept-Charset: 浏览器通过这个头告诉服务器,它支持哪种字符集
  Accept-Encoding:浏览器通过这个头告诉服务器,支持的压缩格式
  Accept-Language:浏览器通过这个头告诉服务器,它的语言环境
  Host:浏览器通过这个头告诉服务器,想访问哪台主机
  If-Modified-Since: 浏览器通过这个头告诉服务器,缓存数据的时间
  Referer:浏览器通过这个头告诉服务器,客户机是哪个页面来的 防盗链
  Connection:浏览器通过这个头告诉服务器,请求完后是断开链接还是何持链接
以application开头的媒体格式类型:
• application/xhtml+xml :XHTML格式
• application/xml : XML数据格式
• application/atom+xml :Atom XML聚合格式
• application/json : JSON数据格式
• application/pdf :pdf格式
• application/msword : Word文档格式
• application/octet-stream : 二进制流数据(如常见的文件下载)
• application/x-www-form-urlencoded : 中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
• multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式
 HTTP响应包括的内容
  一个HTTP响应代表服务器向客户端回送的数据,它包括: 一个状态行、若干消息头、以及实体内容 。
  
 HTTP响应的细节——状态行
  状态行格式: HTTP版本号 状态码 原因叙述
举例:HTTP/1.1 200 OK
  状态码用于表示服务器对请求的处理结果,它是一个三位的十进制数。响应状态码分为5类,如下所示:

 5.3、HTTP响应细节——常用响应头
  HTTP响应中的常用响应头(消息头)
  Location: 服务器通过这个头,来告诉浏览器跳到哪里
  Server:服务器通过这个头,告诉浏览器服务器的型号
  Content-Encoding:服务器通过这个头,告诉浏览器,数据的压缩格式
  Content-Length: 服务器通过这个头,告诉浏览器回送数据的长度
  Content-Language: 服务器通过这个头,告诉浏览器语言环境
  Content-Type:服务器通过这个头,告诉浏览器回送数据的类型
  Refresh:服务器通过这个头,告诉浏览器定时刷新
  Content-Disposition: 服务器通过这个头,告诉浏览器以下载方式打数据
  Transfer-Encoding:服务器通过这个头,告诉浏览器数据是以分块方式回送的
  Expires: -1 控制浏览器不要缓存
  Cache-Control: no-cache
  Pragma: no-cache
**
源码下载地址,不是单独运行的,需要自己集成到项目中修改下:(不知道为啥不能设置免积分)
//download.csdn.net/download/mlichenfeng/11967894

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值