最近需要在我们的安卓设备上实现通过网页访问设备,进行相关配置、上传数据等操作,因此就需要在安卓端实现一个http服务器。(其实代码也可以用于PC端,只不过PC端已经有太多成熟的框架了,JDK7/8之后貌似就内置了一个轻量的HTTP服务器)。
采用java socket实现的http服务器网上有较多的例子,但是例子大部分都比较简单,不具备文件上传的功能,于是结合网上的列子动手写了个具备文件上传、请求资源文件、处理请求的简单的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 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();
}
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