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

来源:互联网 发布:led屏编辑软件 编辑:程序博客网 时间:2024/06/03 22:38

最近需要在我们的安卓设备上实现通过网页访问设备,进行相关配置、上传数据等操作,因此就需要在安卓端实现一个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
**

0 0