公司有个业务需要优化,场景是手机客户端上传小文件到服务端,需要支持断点上传。现在直接用tomcat直接面对客户端进行网络IO,现在准备把网络IO这块剥离出来,彻底交给Nginx+Lua来处理。理由有如下几点:
1、应用服务器还是乖乖的待在后端做业务就好,底层的网络IO跟具体业务无关,这样使得项目的维护更方便,看起来更清晰。
2、对于底层IO操作,不是java的强项,特别是手机用户网速慢的场景,非WIFI用户网速不快,传输时间长,且数据量是间歇性的,时有时无,这种场景使用Nginx的epoll模型最合适不过了。
3、可以使用Nginx做负载均衡进行扩展。
下面写一个小demo来让Nginx+Lua进行网络IO,流程如下:
客户端POST一行数据给Nginx,地址为:http://192.168.112.64:38080/upload,Lua脚本读取数据,如果读取超时,则把当前已读取到的数据POST到后端tomcat,地址为:http://192.168.112.64/GoSmsMarService/Test;如果读取正常,也把读取到的数据POST到后端tomcat。可以根据具体场景直接把POST数据PUSH到缓存服务器,可以提高命中率,因为大多数用户在收到文件后马上会去查看,tomcat服务器再去异步的从缓存服务器读取数据进行持久化。
这里使用了nginx的扩展版本openresty,已经包含了N多第三方模块,当然包括了要使用到的Lua模块,安装过程略。
nginx配置文件如下,Lua代码注释挺详细了,不赘述:
worker_processes 1; error_log logs/error.log; events { worker_connections 1024; } http { # 后端tomcat upstream tomcat { server 127.0.0.1; } server { listen 38080; # tomcat地址 location ~ ^/GoSmsMarService { proxy_store off; proxy_redirect off; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_pass http://tomcat; } # 上传地址 location /upload { default_type text/html; # lua脚本超时设为1秒 lua_socket_read_timeout 1s; content_by_lua ' -- 得到cosocket对象 local sock, err_socket = ngx.req.socket() -- 开始读取 local line, err, partial = sock:receive() -- 如果读一行数据失败 if not line then -- 此次HTTP请求URI设为tomcat后端URI ngx.req.set_uri("/GoSmsMarService/Test") -- 设置post长度 ngx.req.set_header("Content-Length", string.len(partial)); -- 使用读取的数据片段重置post数据 ngx.req.set_body_data(partial) -- 内部请求跳转 return ngx.exec("/GoSmsMarService/Test") end -- 转发完整的post数据,注释参见上面几行 ngx.req.set_uri("/GoSmsMarService/Test") ngx.req.set_header("Content-Length", string.len(line)); ngx.req.set_body_data(line) return ngx.exec("/GoSmsMarService/Test") '; } } }
下面是tomcat接收Nginx的转发请求的servlet代码,把收到的数据写到文件并返回数据长度,代码很简单,不过多解释:
import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.g3net.tool.FileUtils; import com.g3net.tool.WriteStream; public class TestServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ServletInputStream sis = req.getInputStream(); WriteStream ws = new WriteStream(); while (true) { int l = sis.read(); if (l == -1) { break; } ws.write((byte) l); } try { // 将读取到的post数据写到文件系统 FileUtils.write("/data/" + System.currentTimeMillis(), ws.toByteArray()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } // 响应返回读到的post数据的长度 resp.getWriter().print(ws.toByteArray().length); } }
下面写一个测试客户端测试一下读超时的情况,客户端发送post请求到Nginx,并把响应打印出来。post数据是不完整的一行,所以脚本1秒过后就会读超时,代码如下:
import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import com.g3net.tool.WriteStream; public class TestSocket { public static void main(String[] args) throws Exception { Socket socket = new Socket("192.168.112.64", 38080); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); WriteStream ws = new WriteStream(); byte[] data = "post data".getBytes(); StringBuffer sb = new StringBuffer("POST /upload HTTP/1.1\r\n"); sb.append("User-Agent: Java/1.6.0_20\r\n"); sb.append("Host: 192.168.112.64:38080\r\n"); sb.append("Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n"); sb.append("Content-Type: application/octet-stream\r\n"); sb.append("Content-Length: " + data.length + "\r\n"); sb.append("Connection: Close\r\n\r\n"); os.write(sb.toString().getBytes()); os.write(data); os.flush(); while (true) { int l = is.read(); if (l == -1) { break; } ws.writeByte((byte) l); } is.close(); os.close(); socket.close(); System.out.println(new String(ws.toByteArray())); } }
返回结果如下图所示,返回的字节数正好是9个。
再看下文件系统的数据,如下图所示:
把byte[] data = "post data".getBytes();改成byte[] data = "post data complete\r\n".getBytes();
加上换行符就不会出现读超时了,再运行一下,返回的字节发生变化,结果如下图所示:
简单的demo先告一段落,之后会找台好机器测测性能,敬请关注。