Module Proxy中文名称“模块代理”,Rust语言编写的在Tokio和hyper基础上的HTTP中间件,不仅具有HTTP服务器的一般特性,更具有将HTTP协议代理为TCP Socket协议的功能,从而支持更多编程语言进行B/S后端的编程工作。
Module Proxy中间件的架构图:
Module Proxy支持三类代理:文件、HTTP、Socket。其中文件和Http代理同其他常见中间件(如Nginx)相似,本文将重点介绍Module Proxy的特殊能力:Socket代理。
模块的定义
“模块”在Module Proxy中有专有的定义:
URL中的第一段路径,在Module Proxy中定义为模块,如上图中的“module1”。Module Proxy通过配置文件中的模块名配置,代理到具体的后端服务程序。
下面通过一个示例进行讲解。
配置
在Module Proxy配置文件中,定义两个socket模块代理配置,分别命名为socket和socket1。
socket对应后端服务用Java搭建,侦听端口21230
socket1对应后端服务用Golang搭建,侦听端口21231
客户端
index.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.js"></script>
<script type="text/javascript">
$(function(){
// method是和后台服务的约定,表示调研aaa.BBB类的hello方法
var req_json = {
"head":{"method":"aaa.BBB::hello"},
"data":{"hello":"world!!!", "list":[1,2,3,4]}
};
$("#btn").click(function(){
$.ajax({
type: "POST", //传输方式POST
url: "/socket/", //提交URL, socket是模块名
contentType : "application/json; charset=utf-8", //json数据格式,utf-8编码
data: JSON.stringify(req_json),
success: function(rsp_json){
$("#myDiv").html('<h3>'+JSON.stringify(rsp_json)+'</h3>');
}
});
});
});
</script>
</head>
<body>
<button id="btn" type="button">submit</button>
<div id="myDiv"></div>
</body>
</html>
ajax提交req_json到后端服务,注意url定义为“/socket/”和Module Proxy配置对应。req_json分为两部分head和data,head中的method定义了后端接口名称,data是业务数据。json数据的结构需要后端程序协商一致。
Java后端程序
Server.java
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server
{
static final int PORT = 21230;
public static void main(String[] args) throws IOException
{
new Server().serverStart();
}
private void serverStart() throws IOException
{
ServerSocket server = new ServerSocket(PORT);
System.out.println("Server start at port " + PORT);
while (true)
{
Socket socket = server.accept();
new Process(socket).start();
}
}
}
Server启动TCP服务,侦听端口21230,每当accept()接收到客户端请求时,立即启动一个线程Process去处理。
Process.java
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.Map;
import org.codehaus.jackson.map.ObjectMapper;
public class Process extends Thread
{
private Socket socket;
public Process(Socket socket)
{
this.socket = socket;
}
@Override
public void run()
{
try
{
//读取req json长度
byte[] lenBytes = new byte[12];
socket.getInputStream().read(lenBytes);
String lenStr = new String(lenBytes).trim();
int len = Integer.parseInt(lenStr);
System.out.println("req json length: " + len);
//读取req json
byte[] data = new byte[len];
socket.getInputStream().read(data); //这里未考虑tcp的拆包
String jsonStr = new String(data, "utf-8");
//解析req json
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = mapper.readValue(jsonStr, Map.class);
String methodStr = (String) ((Map) map.get("head")).get("method");
String[] methods = methodStr.split("::");
String clazzName = methods[0]; //类名 aaa.BBB
String methodName = methods[1]; //方法名 hello
System.out.println("request class:" + clazzName + ", method:" + methodName);
//java反射调用aaa.BBB类中hello方法
Class<?> clazz = Class.forName(clazzName);
Method method = clazz.getMethod(methodName, Object.class);
String rspJson = (String) method.invoke(clazz.newInstance(), map.get("data"));
//返回 rep json
byte[] rspBytes = rspJson.getBytes();
lenStr = String.format("%010d\r\n", rspBytes.length);
socket.getOutputStream().write(lenStr.getBytes()); //socket返回 长度行
socket.getOutputStream().write(rspJson.getBytes()); //socket返回 rsp json
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
try { socket.close();} catch (IOException e) {}
}
}
}
Process.java是具体处理socket的线程。
它首先读取12字节,这是给以\r\n结尾的长度字符串,表示req_json的长度,这12字节是Module Proxy加上的,方便服务端读取输入json数据。
接下来读取req json,并分析其中的method,得到服务接口的类和方法名称,并通过java反射调用。
最后获取到服务接口返回的rsp json,并包装后返回给Module Proxy。
aaa.BBB.java
package aaa;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import org.codehaus.jackson.map.ObjectMapper;
public class BBB
{
public String hello(Object data) throws Exception
{
Map<String, Object> map = (Map) data;
map.put("time", now());
map.put("module", "java");
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(map);
}
String now()
{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(new Date());
}
}
hello()方法是“真正”的接口服务,前面的Server和Process虽然复杂,但只需编写一次,可以在项目中复用。
Golang后端程序
package main
import (
"encoding/json"
"fmt"
"net"
"strconv"
"strings"
"time"
)
func main() {
listener, err := net.Listen("tcp", "0.0.0.0:21231") //侦听端口21231
if err != nil {
fmt.Println("listen error:", err)
return
}
fmt.Println("server start...")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("accept error:", err)
break
}
go process(conn) //goroutine
}
}
func process(conn net.Conn) {
defer conn.Close()
//读取req json长度
buf := make([]byte, 12) //长度行总是12字节
n, _ := conn.Read(buf)
lenStr := string(buf[:n])
lenStr = strings.Trim(lenStr, "\r\n") //去除行尾的回车换行
lenStr = strings.Trim(lenStr, " ") //去除行左的空格
len, _ := strconv.Atoi(lenStr) //string转int
//读取req json
jsonBuf := make([]byte, len)
n, _ = conn.Read(jsonBuf)
//解析req json
m := make(map[string]interface{}) //map
json.Unmarshal(jsonBuf, &m) //json转map
method := m["head"].(map[string]interface{})["method"]
data := m["data"]
fmt.Println("method: ", method)
fmt.Println("data: ", data)
//调用业务函数
var rspJson []byte
var rsplen int
switch method {
case "aaa.BBB::hello":
rspJson, rsplen = hello(data.(map[string]interface{}))
default:
rspJson, rsplen = foo()
}
//返回 rsp json
lenRsp := fmt.Sprintf("%10d\r\n", rsplen)
conn.Write([]byte(lenRsp)) //socket返回 长度行
conn.Write(rspJson) //socket返回 rsp json
}
func hello(m map[string]interface{}) ([]byte, int) {
m["time"] = time.Now().Format("2006-01-02 15:04:05")
m["module"] = "golang"
b, _ := json.Marshal(m) //map转json
return b, len(b)
}
func foo() ([]byte, int) {
b := []byte("{}")
return b, len(b)
}
Golang功能和前面Java基本一致,这里不再说明。在
上面一堆代码中,hello()和foo()是接口函数,其他代码也是可以复用的。
运行效果
改动客户端JavaScript代码,将模块名 url: "/socket/" 改为 url: "/socket1/",点击submit按钮:
示例到此结束,因篇幅原因,以上代码不是很严谨,比如一些错误或异常并没有去捕获处理,这对服务端程序的稳定运转是不利的,因此这里的代码仅起到示范讲解作用。
Module Proxy的更加详细说明,请看后续章节。