使用 ABAP 实现 TCP Socket 编程 (1) - 客户端部分的实现

文摘   2024-09-19 07:00   四川  

笔者在本科和研究生的计算机专业课上,学习过 Unix 环境下的网络编程,其中就包括用 C 语言实现 TCP 客户端和服务器端。

这是我们当年上课使用的教材。

传输控制协议(Transmission Control Protocol,简称 TCP) 是一种面向连接的、可靠的传输层协议。它确保了数据在网络中可靠地传输,不会出现丢包、重复或者顺序错误的问题。

TCP 协议通过建立连接、数据传输、流量控制、拥塞控制等机制来保证通信的可靠性。它为应用层提供了一种可靠的字节流服务。

套接字(Socket) 是计算机网络中用于进程间通信的一种抽象概念。它为程序提供了一种网络通信的接口,使得应用程序能够通过它发送和接收数据。

套接字有多种类型,包括面向连接的套接字(如 TCP 套接字)和无连接的套接字(如 UDP 套接字)。

在 TCP/IP 详解这本书里,介绍了 TCP 客户端和服务器端建立连接时的三次握手概念。

ABAP Push Channel 框架提供的工具类,对 TCP Socket 的概念进行了高度封装。

ABAP 开发人员不需要了解 TCP 协议的底层技术细节。

使用 ABAP 进行 TCP 客户端的开发,只需要调用 ABAP Channel 框架提供的对应方法,实现建立连接,发送数据到服务器端,以及从服务器端接收数据这三大块需求就可以了。

ABAP Channel 是一种 ABAP 应用服务器与 Internet 通过 Event-Driven 即事件驱动方式进行交互的机制和技术。

ABAP Channel 分为 ABAP Messaging Channel 和 ABAP Push Channel(以下简称 APC) 两类。本文使用的是 ABAP Push Channel 提供的工具类。

SAP 系统已经自带了一个用 APC 实现的 ABAP TCP Client 的报表:DEMO_APC_TCP_CLIENT,总共一百多行的代码。

TCP 服务器端的搭建

既然有客户端,那么一定需要对应的服务器端。该报表使用了一个网络工具领域的瑞士军刀 NCAT,可以在其官网下载:

该下载到本地后是一个 .exe 文件,可以通过命令行指定不同的启动参数,扮演网络客户端和服务器端的不同角色。

如下图所示,报表首先使用 cl_gui_frontend_services 的 get_ip_address 方法,获取本地电脑的 IP 地址。然后在第 14 行,使用 cl_gui_frontend_services 的 execute 方法,调用操作系统的 cmd.exe 命令行工具,启动刚刚下载好的 NCAT 工具的 exe 文件。

-l 参数代表 NCAT 以服务器模式启动,-l 后面跟的参数值,内容为该服务器监听的 IP 地址。-p 参数指定服务器监听的端口号。

ABAP TCP 客户端的实现

使用 cl_apc_tcp_client_manager 的 create 方法,创建一个新的客户端实例。

该方法需要传递服务器端的 IP 地址和端口号,以及一个 TCP Frame 参数。Frame 的意思是帧,其主要任务是提供有关在即将创建的 TCP 连接上发送和接收的数据结构的信息。

接收数据时,APC 框架通过该结构的信息,能够将服务器返回的数据,拆分为将要传送给客户端的 TCP 包。当客户端发送数据到服务器端时,APC 将根据 create 方法指定的帧信息,检查客户端发送的数据格式是否有效。

本例使用了 IF_APC_TCP_FRAME_TYPES=>CO_FRAME_TYPE_TERMINATOR 作为帧类型,意思是由 ABAP 开发人员自行指定一个 TCP 帧的结束符号(Terminator).

上图第六行的 terminator 变量,值为硬编码的 0A,这是换行符 LF(Line Feed)的 Unicode 编码值。

既然 APC 是一种基于事件驱动的通信机制,必然少不了 Event Handler 的实现。

上图第 7 行代码,调用 create 创建 TCP 客户端实例时,还需要给其维护一个事件处理类的实例。

这个事件处理类的实现,语义也很清晰,如上图所示。

  • 基于 ABAP Push Channel 机制实现的事件处理类,需要实现标准接口 if_apc_wsp_event_handler.
  • 该接口声明的 on_open 方法,会在客户端和服务器端成功建立 TCP 连接后自动触发。这个方法内,可以通过输入参数 I_CONTEXT,获得更多的关于成功建立的 TCP 连接上下文信息。
  • 该接口声明的 on_message 方法,在服务器端有数据推送到客户端时自动触发。上图通过 i_message 的 get_text 方法,将服务器端发送回来的字符串数据,存储到类实例的 message 成员变量里。

客户端实例创建成功之后,调用其 connect 方法,即可与服务器建立 TCP 连接。

客户端发送数据到服务器端

下面的代码都是自描述的,语义很容易理解。

1. 调用 client 实例的 get_message_manager 方法,拿到客户端的消息管理器实例。

2. 调用消息管理器的 create_message 方法,生成一条新的空白消息。

3. 将硬编码的 TCP 帧终止符,即换行符的 Unicde Code 0A, 转换成二进制格式。

4. 将用户硬编码的 Hello TCP, answer me! 字符串转换成二进制格式。将第三步换行符的二进制格式添加到其尾部,组装成一个完整的 TCP Frame.

5. 将上一步骤组装好的 TCP Frame,调用消息管理器的 send 方法发送给服务器。

客户端从 TCP 连接读取服务器端发送的响应数据

客户端调用 WAIT FOR PUSH CHANNELS 语句,等待服务器端写入数据到 TCP 连接中。

当服务器端写入数据后,会触发之前提到的客户端事件处理类的 ON_MESSAGE 方法,这个方法会将服务器端响应的字符串内容,保存到事件处理类实例的 message 成员变量中。

这个操作导致逻辑表达式 UNTIL event_handler->message 的值变为 true,因此 ABAP 客户端继续从 WAIT 语句的下一行开始执行。

引入 UP TO 10 SECONDS 即指定 WAIT 的等待时长,最多维持 10 秒。

根据 WAIT 语句的返回值 sy-subrc 判断客户端读取 TCP 连接中的数据是否成功。

  • 0 说明成功读取。
  • 4 说明当前并没有为 APC 注册事件处理。
  • 8 说明 UP TO 指定的 10 秒超时已经到达。

至此这个用 ABAP 编写的 TCP 客户端已经完成,简单测试一下。

执行 DEMO_APC_TCP_CLIENT 报表之后,会看到如下对话框。

TCP_SERVER 显示的是 NCAT 工具的本地路径,然后是本地 IP 地址和端口号。该报表会启动操作系统的 CMD 命令提示行工具,加载 NCAT 工具并监听在这个 IP 地址和端口号,充当 TCP 服务器的角色。

TERMINATOR 0A 即 TCP Frame 终结符的 Unicode 编码,本例我们采用换行符 Line Feed 作为终结符,值为 0A.

MSG 即客户端发送给服务器端的字符串内容。

点击 Enter 按钮,看到 CMD 命令提示行窗口内,已经成功打印出客户端发送过来的 Hello TCP 的字符串,说明通信成功。

此时在 NCAT 的 CMD 窗口内,手动输入一些内容,作为服务器端发送回客户端的数据,比如输入 this is data sent by server.

随即我们在 SAPGUI 的客户端弹出对话框里,收到了这条来自服务器的消息。

如果我们在事件处理类的 on_message 里设置断点,会发现当服务器发送响应数据时,该方法会被 APC 框架触发,并且在调试器里观察收到的数据,确实是以 0A 这个终结符结尾。

后续笔者会介绍更多关于 ABAP Push Channel 的系列内容。

也欢迎大家加入我的知识星球,在里面一起交流 SAP 技术和业务问题。

本文报表的完整源代码:

REPORT zdemo_apc_tcp_client.
CLASS apc_handler DEFINITION FINAL . PUBLIC SECTION. INTERFACES if_apc_wsp_event_handler. DATA message TYPE string.ENDCLASS.
CLASS apc_handler IMPLEMENTATION. METHOD if_apc_wsp_event_handler~on_open. ENDMETHOD.
METHOD if_apc_wsp_event_handler~on_message. TRY. message = i_message->get_text( ). CATCH cx_apc_error INTO DATA(apc_error). message = apc_error->get_text( ). ENDTRY. ENDMETHOD.
METHOD if_apc_wsp_event_handler~on_close. message = 'Connection closed!'. ENDMETHOD.
METHOD if_apc_wsp_event_handler~on_error. ENDMETHOD.ENDCLASS.
CLASS apc_demo DEFINITION. PUBLIC SECTION. CLASS-METHODS main.ENDCLASS.
CLASS apc_demo IMPLEMENTATION. METHOD main. DATA(tcp_server) = `C:\app\nc-tool.exe`. DATA(ip_adress) = cl_gui_frontend_services=>get_ip_address( ). DATA(port) = `12345`. DATA(terminator) = `0A`. DATA(msg) = `Hello TCP, answer me!`.
cl_demo_input=>new( )->add_text( |For the TCP server, download the freely available NCAT.EXE| )->add_field( CHANGING field = tcp_server )->add_field( CHANGING field = ip_adress )->add_field( CHANGING field = port )->add_field( CHANGING field = terminator )->add_field( CHANGING field = msg )->request( ).
IF cl_gui_frontend_services=>file_exist( file = tcp_server ) IS INITIAL. cl_demo_output=>display( 'TCP Server not found!' ). LEAVE PROGRAM. ENDIF. cl_gui_frontend_services=>execute( EXPORTING application = `cmd.exe` parameter = `/c ` && tcp_server && ` -l ` && ip_adress && ` -p ` && port ). WAIT UP TO 1 SECONDS.
TRY. DATA(event_handler) = NEW apc_handler( ).
"Client DATA(client) = cl_apc_tcp_client_manager=>create( i_host = ip_adress i_port = port i_frame = VALUE apc_tcp_frame( frame_type = if_apc_tcp_frame_types=>co_frame_type_terminator terminator = terminator ) i_event_handler = event_handler ).
client->connect( ).
"Send mesasage from client DATA(message_manager) = CAST if_apc_wsp_message_manager( client->get_message_manager( ) ). DATA(message) = CAST if_apc_wsp_message( message_manager->create_message( ) ). DATA(binary_terminator) = CONV xstring( terminator ). DATA(binary_msg) = cl_abap_codepage=>convert_to( msg ). CONCATENATE binary_msg binary_terminator INTO binary_msg IN BYTE MODE. message->set_binary( binary_msg ). message_manager->send( message ).
"Wait for a message from server CLEAR event_handler->message. WAIT FOR PUSH CHANNELS UNTIL event_handler->message IS NOT INITIAL UP TO 10 SECONDS. IF sy-subrc = 4. cl_demo_output=>display( 'No handler for APC messages registered!' ). ELSEIF sy-subrc = 8. cl_demo_output=>display( 'Timeout occured!' ). ELSE. cl_demo_output=>display( |TCP client received:\n\n{ event_handler->message }| ). ENDIF.
client->close( i_reason = 'Application closed connection!' ).
CATCH cx_apc_error INTO DATA(apc_error). cl_demo_output=>display( apc_error->get_text( ) ). ENDTRY.
ENDMETHOD.ENDCLASS.
START-OF-SELECTION. apc_demo=>main( ).

汪子熙
企业管理软件领域开发专家
 最新文章