通过非安全套接字连接读写数据
历史上,套接字默认通过简单连接提供数据传输。以开放形式传输数据支持以技术手段分析所有流量。近年来,安全问题越来越受到重视,因此几乎所有地方都采用了 TLS(传输层安全)技术:TLS 可对发送方和接收方之间的所有数据进行即时加密。特别地,对于互联网连接,区别在于 HTTP(简单连接)和 HTTPS(安全)协议。
MQL5 为处理简单连接和安全连接提供了不同组的 Socket 函数。在本节中,我们将熟悉简单模式,稍后我们将转向受保护的模式。
要从套接字读取数据,可使用 SocketRead 函数。
int SocketRead(int socket, uchar &buffer[], uint maxlen, uint timeout)
套接字描述符从 SocketCreate 获取,并使用 Socket Connect连接到网络资源。
buffer 参数是对数据将读入的数组的引用。如果数组是动态的,其大小会按读取的字节数增加,但不能超过 INT_MAX (2147483647)。你可以在 maxlen 参数中限制读取字节的数量。无法容纳的数据将保留在套接字的内部缓冲区中:它可以通过后续调用 SocketRead 来获取。maxlen 的值必须在 1 和 INT_MAX (2147483647) 之间。
timeout 参数指定等待读取完成的时间(以毫秒为单位)。如果在此时间内未收到数据,则尝试终止,函数以结果 -1 退出。
发生错误时也返回 -1,而 _LastError 中的错误代码,例如 5273 (ERR_NETSOCKET_IO_ERROR),表示通过 SocketConnect 建立的连接现已断开。
如果成功,函数返回读取的字节数。
将读取超时设置为 0 时,使用默认值 120000(2 分钟)。
要向套接字写入数据,请使用 SocketSend 函数。
遗憾的是,函数名 SocketRead 和 SocketSend 并不“对称”:"read" 的反向操作是 "write","send" 的反向操作是 "receive"。在其他平台上有网络 API 开发经验的开发者可能对此很陌生。
int SocketSend(int socket, const uchar &buffer[], uint maxlen)
第一个参数是先前创建和打开的套接字的句柄。传递无效句柄时,_LastError 会收到错误 5270 (ERR_NETSOCKET_INVALIDHANDLE)。buffer 数组包含要发送的数据,数据大小在 maxlen 参数中指定(引入该参数是为了方便从固定数组发送部分数据)。
函数成功时返回写入套接字的字节数,错误时返回 -1。
系统级错误(5273,ERR_NETSOCKET_IO_ERROR)表示断开连接。
SocketReadWriteHTTP.mq5 脚本演示了如何使用套接字通过通过 HTTP 协议来执行工作,即从 Web 服务器请求有关页面的信息。这是 WebRequest 函数在后台为我们执行的一部分工作。
我们在输入参数中保留默认地址:站点 "www.mql5.com"。选择端口号 80,因为这是非安全 HTTP 连接的默认值(尽管某些服务器可能使用不同的端口:81、8080 等)。此示例尚不支持为安全连接保留的端口(特别是最常用的 443)。此外,在Server参数中,重要的是输入域名而不是特定页面,因为该脚本只能请求主页面,即根路径 "/"。
input string Server = "www.mql5.com";
|
在脚本的主函数中,我们将创建一个套接字并使用指定的参数在其上打开一个连接(超时为 5 秒)。
void OnStart()
|
我们看一下 HTTP 协议是如何工作的。客户端以特殊设计的标头(具有预定义名称和值的字符串)的形式发送请求,其中特别包括网页地址,而服务器则以整个网页或操作状态作为响应,同样也使用特殊的标头。客户端可以使用 GET 请求请求网页,使用 POST 请求发送一些数据,或者使用精简的 HEAD 请求检查网页状态。理论上,还有更多 HTTP 方法―你可以在 HTTP 协议规范中详细了解。
因此,脚本必须通过套接字连接生成并发送一个 HTTP 标头。在最简单的形式中,下面的 HEAD 请求允许你获取页面的元信息(我们可以用 GET 代替 HEAD 来请求整个页面,但会有一些复杂的问题,我们稍后再讨论)。
HEAD / HTTP/1.1
|
"HEAD"(或其他方法)之后的正斜杠是任何服务器上到根目录的最短可能路径,这通常会导致显示主页。如果我们想要一个特定的网页,我们可以写入类似 "GET /en/forum/ HTTP/1.1" 的内容,并从 mql5.com 获取英文论坛的目录。请用实际域名替换 "_server_" 字符串。
尽管 "User-Agent:" 标头是可选的,但这个标头允许程序向服务器进行自我介绍,若没有它,某些服务器可能会拒绝请求。
注意两个空行:它们标记了标头的结束。在我们的脚本中,用以下表达式形成标题很方便:
StringFormat("HEAD / HTTP/1.1\r\nHost: %s\r\n\r\n", Server) |
现在,我们只需将其发送到服务器。为此,我们编写了一个简单的函数 HTTPSend。它接收一个套接字描述符和一个标头行。
bool HTTPSend(int socket, const string request)
|
在内部,我们将字符串转换为字节数组并调用 SocketSend。
接下来,我们需要接收服务器的响应,为此我们编写了 HTTPRecv 函数。它也需要一个套接字描述符和对一个应放置数据的字符串的引用,但更为复杂。
bool HTTPRecv(int socket, string &result, const uint timeout)
|
在这里,我们在循环中检查指定超时内出现的数据,并将其读入 response 缓冲区。发生错误会终止循环。
缓冲区字节立即转换成字符串并连接到 result 变量中的完整响应。务必注意,对于 HTTP 标头,我们只能使用具有默认编码的 CharArrayToString 函数,因为它只允许使用拉丁字母和 ANSI 中的一些特殊字符。
完整的 Web 文档通常采用 UTF-8 编码(但可能采用其他非拉丁编码,这恰好在 HTTP 标头中指明),要接收完整的 Web 文档,将需要更复杂的处理:首先,你需要将所有发送的块收集到一个公共缓冲区中,然后指明 CP_UTF8 将整个内容转换为字符串(否则,任何以双字节编码的字符在发送时都可能被“切割”,并分别到达不同的块中;这就是为什么我们不能期望在单个片段中获得正确的 UTF-8 字节流)。我们将在以下部分改进此示例。
有了 HTTPSend 和 HTTPRecv 函数,我们就完成了 OnStart 代码。
void OnStart()
|
在从服务器接收到的 HTTP 标头中,我们可能会对以下几行感兴趣:
- 'Content-Length:' 文档总长度,以字节为单位
- 'Content-Language:' 文档语言(例如,"de-DE, ru")
- 'Content-Type:' 文档编码(例如,"text/html; charset=UTF-8")
- 'Last-Modified:' 文档的最后修改时间,这样就不会下载已经存在的内容(原则上,我们可以在 HTTP 请求中添加 'If-Modified-Since:' 标头)
我们将更详细地讨论如何确定文档长度(数据大小),因为几乎所有标头都是可选的,也就是说,它们是由服务器随意报告的,在它们缺失的情况下,会使用替代机制。知道大小对于何时关闭连接非常重要,即确保所有数据都已接收。
使用默认参数运行该脚本会产生以下结果。
Server=www.mql5.com / ok
|
请注意,这个站点,像今天大多数站点一样,会将我们的请求重定向到一个安全连接:这是通过状态码 "301 Moved Permanently" 和新地址 "Location: https://www.mql5.com/" 实现的(这里的 "https" 协议很重要)。要重试一个启用了 TLS 的请求,必须使用其他一些函数,我们稍后会讨论它们。