如何在nestjs下封装soap方式的请求

242 阅读7分钟

SOAP介绍(链接

SOAP(Simple Object Access Protocol)一般指简单对象访问协议,简单对象访问协议是交换数据的一种协议规范,是一种轻量的、简单的、基于XML(标准通用标记语言下的一个子集)的协议,它被设计成在WEB上交换结构化的和固化的信息.## 核心技术  SOAP采用了已经广泛使用的两个协议:HTTP 和XML(标准通用标记语言下的一个子集)。HTTP用于实现 SOAP 的RPC 风格的传输, 而XML 是它的编码模式。采用几行代码和一个XML 解析器, HTTP 服务器( MS 的 IIS 或 Apache) 立刻成为SOAP 的 ORBS。SOAP 通讯协议使用 HTTP 来发送XML 格式的信息。HTTP与RPC 的协议很相似,它简单、 配置广泛,并且对防火墙比其它协议更容易发挥作用。HTTP 请求一般由 Web 服务器软件(如 IIS 和Apache)来处理, 但越来越多的应用服务器产品正在支持HTTP。XML 作为一个更好的网络数据表达方式( NDR)。SOAP 把 XML 的使用代码化为请求和响应参数编码模式, 并用HTTP 作传输。具体地讲, 一个SOAP 方法可以简单地看作遵循SOAP编码规则的HTTP请求和响应, 一个 SOAP终端则可以看作一个基于HTTP 的URL, 它用来识别方法调用的目标。像CORBA/ IIOP一样, SOAP不需要具体的对象绑定到一个给定的终端, 而是由具体实现程序来决定怎样把对象终端标识符映像到服务器端的对象。

优点

1.可扩展的。SOAP 无需中断已有的应用程序, SOAP 客户端、 服务器和协议自身都能发展。而且SOAP 能极好地支持中间介质和层次化的体系结构。

2.简单的。客户端发送一个请求,调用相应的对象, 然后服务器返回结果。这些消息是XML 格式的,并且封装成符合HTTP 协议的消息。因此,它符合任何路由器、 防火墙或代理服务器的要求。

3.完全和厂商无关。SOAP可以相对于平台、 操作系统、 目标模型和编程语言独立实现。另外,传输和语言绑定以及数据编码的参数选择都是由具体的实现决定的。

4.与编程语言无关。SOAP 可以使用任何语言来完成,只要客户端发送正确SOAP 请求( 也就是说, 传递一个合适的参数给一个实际的远端服务器)。SOAP 没有对象模型,应用程序可以捆绑在任何对象模型中。

5.与平台无关。SOAP 可以在任何操作系统中无需改动正常运行。

为什么使用SOAP?

对于应用程序开发来说,使程序之间进行因特网通信是很重要的。

目前的应用程序通过使用远程过程调用(RPC)在诸如 DCOM 与 CORBA 等对象之间进行通信,但是 HTTP 不是为此设计的。RPC 会产生兼容性以及安全问题;防火墙和代理服务器通常会阻止此类流量。

通过 HTTP 在应用程序间通信是更好的方法,因为 HTTP 得到了所有的因特网浏览器及服务器的支持。SOAP 就是被创造出来完成这个任务的。

SOAP 提供了一种标准的方法,使得运行在不同的操作系统并使用不同的技术和编程语言的应用程序可以互相进行通信。

我的需求是在工业领域与零件设备QT方面进行通信,故有如下实践过程

1. SOAP在Nest.js下的封装结构

  • server // 作为服务端接受客户端的soap请求
    • modules
      • soap
        • soap.controller.ts // 进行路由操作
import {Controller, Get, Post, Req, Res} from '@nestjs/common';
import { SoapService } from './soap.service';

@Controller('soap')
export class SoapController {
	constructor(private readonly soapService: SoapService) {}

	@Get('hh')
	async handleSoapRequest(@Req() req, @Res() res) {
		try {
			res.send({
				name: 'yang',
				age: 18
			})
		} catch (error) {
			console.error('Failed to handle SOAP request:', error);
			res.status(500).send('Internal Server Error');
		}
	}

	@Post('client')
	public async soapclientRe (@Req() req: any, @Res() res: any): Promise<any> {
		// console.log(req, res)
		const result = await this.soapService.getClient(req)
		return res.status(200).send(result);
	}
}

        - soap.module.ts // 引入需要模块
import { Module } from '@nestjs/common';
import { SoapServer } from './soap.server';
import { SoapService } from './soap.service';
import {SoapController} from "./soap.controller";
import {TypeOrmModule} from "@nestjs/typeorm";
import {User} from "../../entitys";

@Module({
    controllers: [SoapController],
    providers: [SoapService],
    imports: [
        SoapServer,
        TypeOrmModule.forFeature([User], 'security')
    ],
    exports: [SoapServer]
})
export class SoapModule {
    // constructor(private readonly soapServer: SoapServer) {}
    //
    // async onModuleInit(): Promise<void> {
    //     await this.soapServer.start();
    // }
    //
    // async onModuleDestroy(): Promise<void> {
    //     await this.soapServer.stop();
    // }
}

        - soap.server.ts // 定义服务的接口名称信息,只有定义了的接口的soap协议请求才会接收到请求
// service.provider.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class SoapServer {
    async getService(): Promise<any> {
        return {
            Service_one: {
                // 定义接口
                TestPort: {
                    func_1: function (args) {
                        console.log('func_1', args);
                        return {
                            name: args.name
                        };
                    },

                    func_2: function (args, callback) {
                        console.log('func_2', args);

                        callback({
                            name: args.name
                        });
                    },

                    func_3: function (args) {
                        console.log('func_3', args);
                        return new Promise((resolve) => {
                            resolve({
                                name: args.name,
                                async: true
                            });
                        });
                    },

                    func_4: function (args, cb, headers, req) {
                        console.log('func_4', args);
                        console.log(headers);
                        console.log('SOAP `reallyDetailedFunction` request from ' + req.connection.remoteAddress);
                        return {
                            name: headers.Token
                        };
                    }
                }
            }
        };
    }
}

        - soap.service.ts // 处理业务逻辑
import {Inject, Injectable} from '@nestjs/common';
import {soapRequest} from "../../utils/soap_request";

@Injectable()
export class SoapService {

    async getClient(req: any) {
        return await soapRequest.postSoapRequest('func_2', {name: 1111})
    }
}

  • client // 用于测试服务端soap接口
const soap = require("soap");
var url = "http://127.0.0.1:8000/wsdl?wsdl";
function cb(err, res) {
    console.log("cb", res);
}

async function main() {
    soap.createClient(url, async (err, client) => {
        if (err) {
            console.log(err, "ee");
        } else {
            console.log(client,'----');
            client.addSoapHeader({ Token: "test" });
            console.log(client.describe());
            client.func_1({ name: "test" }, cb);
        }
    });
}

main();
  • utils
import {createClient} from "soap";
import * as nconf from 'nconf'
nconf.argv().env().file({ file: 'configMap.json' })
const URL = nconf.get('soap_client_url')
class SoapClientRequest {
    private client: any
    private static instance: any;

    constructor() {
        if (!this.client) {
            // 无客户端创建客户端
            console.log(URL, '@@@@@')
            createClient(URL, (err, client) => {
                if (err) {
                    console.warn('soap客户端创建失败')
                    console.log('soap客户端创建失败')
                } else {
                    console.log('soap客户端创建成功')
                    client.addSoapHeader({
                        "Content-Type": "application/xml",
                        "Connection": "keep-alive"
                    })
                    client.describe()
                    this.client = client
                }
            })
        }
        return this.client ?? null

    }

    public static getInstance () {
        console.log('11111111')
        if (!this.instance) {
            // 无创建实例,则创建

            console.log('------------')
            this.instance = new SoapClientRequest()
        }
        return this.instance
    }

    public async postSoapRequest (method: string, data: any): Promise<any> {
        console.log(method, data)
        console.log(this.client);
        return await new Promise((resolve, reject) => {
            this.client[method](data, (err, res) => {
                if (err) {
                    reject(err)
                }
                resolve(res)
            })
        })
    }
}
export const soapRequest = SoapClientRequest.getInstance()

  • app.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { createServer } from 'http';
import * as soap from 'soap';
import {ConfigService} from "@nestjs/config";
import * as fs from "fs";
import * as path from "path";
import {SoapServer} from "./modules/soap/soap.server";
import {soap_printf, start_printf} from "./utils/start_printf";
async function bootstrap() {
	const app = await NestFactory.create(AppModule);
	/**
	 * 读取wsdl文件
	 */
	const xml = fs.readFileSync(path.join(process.cwd() + '/soap.wsdl'), 'utf8');
	/**
	 * 设置全局路由前缀
	 */
	app.setGlobalPrefix(`${process.env.NODE_ENV}/security`)

	const configService = app.get(ConfigService)
	/**
	 * 开启主服务,端口为8000
	 */
	await app.listen(configService.get('SERVE_LISTENER_PORT'))
	/**
	 * 获取主服务代码创建
	 */
	const server = createServer(app.getHttpAdapter().getInstance());
	/**
	 * 创建soap通信服务,端口为8000
	 */
	await server.listen(configService.get('SERVE_SOAP_PORT'));
	const soapServer = app.get(SoapServer)
	const soaps = await soapServer.getService()
	await soap.listen(server, '/wsdl', soaps, xml, soap_printf);
}
bootstrap().then(() => {
	start_printf()
});

  • soap.wsdl
<?xml version="1.0"?>
<!--
  xmlns:xsd1
可以理解为别名,具体可以认为xsd1代表了命名空间为http://example.com/stockquote.xsd下的属性多定义,即xsd1:WriteObjectRequest代表了
  targetNamespace="http://example.com/stockquote.xsd 下的属性(element)WriteObjectRequest
-->
<wsdl:definitions name="StockQuote" targetNamespace="http://example.com/stockquote.wsdl"
                  xmlns:tns="http://example.com/stockquote.wsdl" xmlns:xsd1="http://example.com/stockquote.xsd"
                  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">

    <wsdl:types>
        <xsd:schema targetNamespace="http://example.com/stockquote.xsd"
                    xmlns:xsd="http://www.w3.org/2000/10/XMLSchema">
            <xsd:element name="funcRequest">
                <xsd:complexType>
                    <xsd:all>
                        <!-- 定义复杂参数内的参数名以及类型 -->
                        <xsd:element name="name" type="xsd:string" />
                        <xsd:element name="age" type="xsd:interge" />
                    </xsd:all>
                </xsd:complexType>
            </xsd:element>
            <xsd:element name="funcResponse">
                <xsd:complexType>
                    <xsd:all>
                        <xsd:element name="name" type="xsd:string" />
                    </xsd:all>
                </xsd:complexType>
            </xsd:element>
            <xsd:element name="funcAsyncResponse">
                <xsd:complexType>
                    <xsd:all>
                        <xsd:element name="name" type="xsd:object" />
                    </xsd:all>
                </xsd:complexType>
            </xsd:element>
        </xsd:schema>
    </wsdl:types>

    <!--定义了输出或者接受消息的消息格式  -->
    <wsdl:message name="FuncInput">
        <wsdl:part name="body" element="xsd1:funcRequest" />
    </wsdl:message>
    <wsdl:message name="FuncOutput">
        <wsdl:part name="body" element="xsd1:funcResponse" />
    </wsdl:message>
    <wsdl:message name="funcAsyncOutput">
        <wsdl:part name="body" element="xsd1:funcAsyncResponse" />
    </wsdl:message>

    <!-- PortType定义了一些抽象的操作。PortType中的operation元素定义了调用PortType中所有方法的语法
    每一个operation元素声明了方法的名称、参数(使用<message>元素)和各自的类型(<part>元素要在所有<message>中声明)。
    在<operation>元素中,可能会有至多一个<input>元素,一个<output>元素,以及一个<fault>元素。三个元素各有一个名字和一个消息属性。
    <operation>可以同名,使用内部的传参不同来表示方法的重载
  -->
    <wsdl:portType name="TestPortType">
        <wsdl:operation name="func_1">
            <wsdl:input message="tns:FuncInput" />
            <wsdl:output message="tns:FuncOutput" />
        </wsdl:operation>
        <wsdl:operation name="func_2">
            <wsdl:input message="tns:FuncInput" />
            <wsdl:output message="tns:FuncOutput" />
        </wsdl:operation>
        <wsdl:operation name="func_3">
            <wsdl:input message="tns:FuncInput" />
            <wsdl:output message="tns:FuncOutput" />
        </wsdl:operation>
        <wsdl:operation name="func_4">
            <wsdl:input message="tns:FuncInput" />
            <wsdl:output message="tns:FuncOutput" />
        </wsdl:operation>
    </wsdl:portType>

    <!-- Binding栏是完整描述协议、序列化和编码的地方,Types,
    Messages和PortType栏处理抽象的数据内容,而Binding栏是处理数据传输的物理实现。Binding栏把前三部分的抽象定义具体化
  1:定义一个name,用于被Services栏的<port>元素引用,type元素引用了portType栏。
  2:soap:binding 指定了所使用的风格("rpc")和传输方式,这里指定了传输方式为HTTP SOAP协议
  3: <operation>中soapAction定义了请求的URI,<operation>属性可以包含<input>, <output>
    和<fault>的元素,它们都对应于PortType栏中的相同元素。这三个元素中的每一个可有一个可选的"name"属性,<input>元素中有一个<soap:body>元素,它指定了哪些信息被写进SOAP消息的信息体中。该元素有以下属性:
  Use
   用于制定数据是"encoded"还是"literal"。"Literal"指结果SOAP消息包含以抽象定义(Types, Messages,
    和PortTypes)指定格式存在的数据。"Encoded"指"encodingStyle"属性决定了编码方式。
  Namespace
   每个SOAP消息体可以有其自己的namespace来防止命名冲突。这一属性制定的URI在结果SOAP消息中逐字使用。
  EncodingStyle
   对SOAP编码 http://schemas.xmlsoap.org/soap/encoding
  -->
    <wsdl:binding name="TestBinding" type="tns:TestPortType">
        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" />
        <!-- <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/webSocket" /> -->
        <wsdl:operation name="func_1">
            <soap:operation soapAction="http://172.20.10.9:8000/func_1" />
            <wsdl:input>
                <soap:body use="literal" />
            </wsdl:input>
            <wsdl:output>
                <soap:body use="literal" />
            </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="func_2">
            <soap:operation soapAction="http://172.20.10.9:8000/func_2" />
            <wsdl:input>
                <soap:body use="literal" />
            </wsdl:input>
            <wsdl:output>
                <soap:body use="literal" />
            </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="func_3">
            <soap:operation soapAction="http://172.20.10.9:8000/func_3" />
            <wsdl:input>
                <soap:body use="literal" />
            </wsdl:input>
            <wsdl:output>
                <soap:body use="literal" />
            </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="func_4">
            <soap:operation soapAction="http://172.20.10.9:8000/func_4" />
            <wsdl:input>
                <soap:body use="literal" />
            </wsdl:input>
            <wsdl:output>
                <soap:body use="literal" />
            </wsdl:output>
        </wsdl:operation>
    </wsdl:binding>

    <!-- service是一套<port>元素
    在一个WSDL文档中,<service>的name属性用来区分不同的service。因为同一个service中可以有多个端口,它们也有"name"属性。
  -->
    <wsdl:service name="Service_one">
        <wsdl:port name="TestPort" binding="tns:TestBinding">
            <soap:address location="http://172.20.10.9:8000/wsdl" />
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>

文中通过服务端server进行定义可被系统接受的路由接口, 然后在项目启动中加载xml配置文件表示为一个可以接受soap协议请求的服务


参考

www.cnblogs.com/wuxiumu/p/1…

www.w3school.com.cn/soap/soap_i…

http协议与soap协议之间的区别

baike.baidu.com/item/%E7%AE…