Module Proxy (二)初探

原创
2021/09/29 23:50
阅读数 1.6K

Module Proxy

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的更加详细说明,请看后续章节。

展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部