在很多需要数据库主动对外通信的场景中,比如数据库定时任务直接把采集结果推送给消息网关,或者存储过程需要调用一个只提供TCP原始 Socket接口的老旧设备服务,utl_tcp包几乎是唯一的选择。它属于Oracle内置的PL/SQL包,无需额外安装任何组件,就能让数据库作为TCP客户端与外部服务器建立连接、收发字节流。本文围绕utl_tcp的核心用法展开,覆盖连接管理、数据读写、编码处理以及常见报错的排查思路。

utl_tcp是什么以及它的通信模型
utl_tcp是Oracle提供的一个系统内置包,位于SYS用户下,普通用户使用前需要由DBA显式授权。它把底层Socket操作封装成了PL/SQL函数和过程,开发者不需要了解操作系统的Socket API,只需要调用open_connection打开连接,再用write_text、read_text等接口读写数据即可。
需要明确的是,utl_tcp实现的是TCP协议层面的字节流通信,它本身不理解HTTP、FTP这类应用层协议。如果你想通过它调用一个HTTP接口,就需要自己按照HTTP报文格式拼接请求头和请求体,再用write_text发出去。换句话说,它给你的是一根管道,管道里传什么内容、按什么格式组织,完全由你自己定义。
从通信角色上看,utl_tcp主要用于让数据库作为客户端主动连接外部服务器,也就是主动发起连接的一方。这一点在排查网络问题时很重要:数据库服务器必须能通过网络访问到目标地址,监听防火墙需要放行出站方向的目标端口。
建立连接与读写数据的基本用法
使用utl_tcp的第一步是授权。DBA执行下面这条语句后,业务用户才能调用包中的程序单元:
GRANT EXECUTE ON SYS.UTL_TCP TO your_user;
授权之后,建立连接的核心是open_connection函数。它接收远端主机、端口、字符集、超时时间等参数,返回一个连接句柄。下面是一段完整可运行的示例代码,演示连接、发送一段文本、读取响应、关闭连接的全过程:
DECLARE
c utl_tcp.connection;
n PLS_INTEGER;
v_resp VARCHAR2(2000);
BEGIN
-- 打开到目标服务器的连接,本地端口随机分配,超时30秒
c := utl_tcp.open_connection(
remote_host => '192.168.1.100',
remote_port => 9000,
local_host => NULL,
local_port => NULL,
in_timeout =>30,
charset => 'AL32UTF8'
);
-- 发送一段文本报文,末尾追加换行符作为分隔
n := utl_tcp.write_text(c, 'HELLO|ORDER123|TEST' || utl_tcp.crlf);
utl_tcp.flush(c);
-- 读取对方返回的数据
BEGIN
LOOP
n := utl_tcp.available(c);
EXIT WHEN n <= 0;
v_resp := v_resp || utl_tcp.read_text(c);
END LOOP;
EXCEPTION
WHEN utl_tcp.end_of_input THEN
NULL; -- 对方关闭了输出流,正常结束
END;
dbms_output.put_line('收到响应: ' || v_resp);
-- 释放连接,这一步非常关键
utl_tcp.close_connection(c);
EXCEPTION
WHEN OTHERS THEN
BEGIN
utl_tcp.close_connection(c);
EXCEPTION WHEN OTHERS THEN NULL;
END;
dbms_output.put_line('通信失败: ' || SQLERRM);
END;
这段代码有几个细节值得注意。write_text返回实际写入的字节数,如果网络缓冲区已满,写入可能不完整,严谨的场景下应该循环写入直到全部发出。read_text在读取到流末尾时会抛出end_of_input异常,所以读取逻辑要包在异常处理里,不能当作普通错误对待。flush的作用是强制把缓冲区数据发送出去,某些对端在收到完整报文前不会回应,漏掉flush会导致程序看似卡死。
关于超时参数in_timeout,它同时作用于连接建立阶段和数据读写阶段。生产环境强烈建议设置一个合理值,否则一旦对端服务僵死,PL/SQL会话会长时间挂在Socket调用上,占用数据库进程资源。如果不设置,默认值可能是无限等待,这在724小时运行的定时任务里是很危险的。
中文编码与二进制数据的处理
编码问题是utl_tcp实践中出错率最高的环节之一。open_connection的charset参数决定了write_text和read_text在字符与字节之间转换时使用的字符集。如果对端是Java服务且使用UTF-8,数据库也是UTF-8,直接指定AL32UTF8即可;但如果数据库字符集是GBK而对方是UTF-8,就必须在连接时显式指定AL32UTF8,否则中文明明在数据库里显示正常,发出去就变成乱码。
除了charset参数,还可以通过convert函数或者write_raw接口手动控制字节流。当协议中包含报文头(比如前四个字节表示报文体长度)时,混合使用write_raw和write_text会很常见,例如先用write_raw发送4字节的长度标识,再发送文本内容。下面的片段演示了这种典型的长度前缀协议写法:
DECLARE
c utl_tcp.connection;
v_msg VARCHAR2(500) := '{"cmd":"push","data":"测试数据"}';
v_len BINARY_INTEGER;
BEGIN
c := utl_tcp.open_connection(remote_host => '192.168.1.100',
remote_port => 9000,
charset => 'AL32UTF8',
in_timeout => 30);
-- 计算报文体按UTF-8编码后的字节长度
v_len := length(convert(v_msg, 'AL32UTF8'));
-- 先发4字节长度头,再发报文体
utl_tcp.write_raw(c, utl_raw.cast_to_raw(chr(bitand(v_len/16777216,255)))
|| utl_raw.cast_to_raw(chr(bitand(v_len/65536,255)))
|| utl_raw.cast_to_raw(chr(bitand(v_len/256,255)))
|| utl_raw.cast_to_raw(chr(bitand(v_len,255))));
utl_tcp.write_text(c, v_msg);
utl_tcp.flush(c);
utl_tcp.close_connection(c);
END;
读二进制数据时对应使用read_raw,它按字节读取,不做任何字符集转换,适合处理图片、加密报文或者自定义二进制协议。读出来的RAW类型可以用utl_raw包做拼接、截取和比较,最后再用rawtohex或utl_raw.cast_to_varchar2转换成可读形式。
权限控制与常见问题排查
从Oracle 11g开始,数据库对网络访问做了严格的ACL限制,即使给用户授予了utl_tcp的执行权限,如果目标主机不在ACL列表里,连接依然会报ORA-24247网络访问被拒绝的错误。解决方法是使用dbms_network_acl_admin包创建访问控制列表并授权:
BEGIN
dbms_network_acl_admin.create_acl(
acl => 'tcp_acl.xml',
description => 'utl_tcp访问权限',
principal => 'YOUR_USER',
is_grant => TRUE,
privilege => 'connect');
dbms_network_acl_admin.assign_acl(
acl => 'tcp_acl.xml',
host => '192.168.1.100',
lower_port => 9000,
upper_port => 9000);
COMMIT;
END;
这里的host和port范围要与实际访问的目标一致,如果应用需要连接多台服务器,可以多次调用assign_acl绑定不同的主机端口段。12c之后推荐改用dbms_network_acl_admin.append_host_ace,写法略有不同但原理一致。
实际排障时,可以把常见错误归类记忆:ORA-29260表示网络读取超时,通常是in_timeout设置过短或对端响应慢;ORA-29261是写入失败,多与连接已被对端关闭有关;ORA-12535这类TNS超时则说明根本没建立起TCP连接,问题在网络层而不是PL/SQL代码。遇到连接不通时,先用telnet从数据库服务器上验证端口连通性,可以快速区分是代码问题还是网络问题。
另一个高频问题是连接泄漏。utl_tcp的连接在会话级别维护,如果代码中途抛异常而没有走到close_connection,该连接会一直挂着,直到会话断开才释放。长连接池化的应用如果频繁泄漏,最终会耗尽数据库服务器本地的端口资源。因此所有utl_tcp代码都应该遵循异常分支里也关闭连接的写法,也就是前面示例中exception部分先尝试close再处理错误的模式,这一点务必养成习惯。
总结来看,utl_tcp的功能并不复杂,核心就是打开连接、读写数据、关闭连接三步,但编码、超时、ACL、连接释放这四个环节每一个都有各自的坑。把这些细节处理到位,用PL/SQL实现稳定可靠的数据库级Socket通信并不是难事。
Oracle utl_tcp套接字通信TCP连接修改时间:2026-09-03 23:03:16