首页 > 解决方案 > 在 Windows 中使用 Visual Studio 通过 C 代码发送 http URL

问题描述

我有一个视频编码器,它可以通过发送 URL 来开始/停止录制。

开始录制:http: //192.168.1.5/blah-blah/record/record.cgi

停止录制:http: //192.168.1.5/blah-blah/record/stop.cgi

我不必担心得到回应。我只需要发送它。

我正在探索 GET 方法并尝试使用HappyHTTP库。有没有不使用任何库的简单方法?

我在 Windows 上使用视觉工作室。

标签: csocketshttpvisual-studio-2010

解决方案


向服务器发送请求的最简单的方式是“”“非常简单的”“”。

首先,我假设您知道 url 的确切主机和端口。我假设这是因为我无法访问您提供的链接。在大多数情况下,您知道主机是您在 url 中键入的域,端口是 http 的 80 或 https 的 443 - 但是对于给定的链接,我无法确认这一点。

在任何情况下,首先要做的事情是——你需要在 Windows 上进行套接字编程的头文件是——

#include <WinSock2.h>
#include <WS2tcpip.h>

您还需要链接 winsock2 库,您可以通过#pragma comment(lib, "Ws2_32.lib")在标题后添加来做到这一点。或者你可以放入Ws2_32.libVisual Project Configuration -> Linker -> Input -> Additional DependenciesStudio。

现在,winsock2 要求您启动库(并关闭它)。理想情况下,您可以在您的main函数中执行此操作,因为要建立任何套接字连接都需要启动 -

 /* Initialize wsock2 2.2 */
WSADATA wsadat;
if (WSAStartup(MAKEWORD(2, 2), &wsadat) != 0)
{
    printf("WSAStartup failed with error %d\n", WSAGetLastError());
    return 1;
}

一切完成后——

/* Cleanup wsock2 */
if (WSACleanup() == SOCKET_ERROR)
{
    printf("WSACleanup failed with error %d\n", WSAGetLastError());
    return 1;
}

现在您需要获取有关您要连接的服务器的信息,我们使用getaddrinfo. (相关的posix文档

static struct addrinfo const hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM };

struct addrinfo* addrs;
int gai_retcode = getaddrinfo("192.168.1.5", "http", &hints, &addrs);
if (gai_retcode != 0)
{
    fprintf(stderr, "Error encountered during getaddrinfo: %s\n", gai_strerrorA(gai_retcode));
    return 1;
}

让我们稍微介绍一下,getaddrinfo接受一个node参数 - 它是主机名字符串或 IP 地址字符串。

第二个参数是service- 它是一个字符串,指定要连接的端口或正在使用的服务(httphttpstelnet- 您可以在IANA 端口列表/etc/services您的 linux 框中找到服务列表)。

第三个参数是包含“提示”的结构的地址,以帮助选择正确的地址。 addrinfo为了连接到服务器,您通常只需要设置ai_familyandai_socktype成员,其他所有内容都应初始化为 0(这是复合初始化程序自动执行的操作)。AF_UNSPEConai_family表示“找到我的 ipv4 或 ipv6 地址 - 没有特殊要求”,SOCK_STREAMonai_socktype表示“找到我的流服务器地址”(也称为 TCP 服务器,而不是数据报服务器)。HTTP 是一种基于流的协议。

最后一个参数是指向结构的指针的地址 addrinfo。这是存储查找结果的地方。

getaddrinfo成功返回 0 并填充指向指针的addrs指针。addrs现在是从查找返回的地址的链接列表 - 列表中的第一个可以直接访问,下一个可以通过.ai_next等等访问,直到您到达NULL. 常规的恶作剧。

对于在世界各地拥有多个服务器的网站,getaddrinfo将返回许多地址。它们以您的机器确定的最相关的方式排列。也就是说,第一个是最有可能运行良好的地址。但理想情况下,您应该尝试链接列表中的下一个,以防第一个失败。

对于非零返回,您可以使用gai_strerror打印错误消息。gai_strerrorA无论出于何种原因,它都在 Windows 上命名。gai_strerror在 windows上也有 a ,但它返回 aWCHAR*而不是 a char*gai_strerror在 linux 上,您将使用gai_strerrorA. 相关的 linux 文档gai_strerror

现在您有了地址列表,您可以创建一个套接字并连接到其中一个 -

int sfd = socket(addrs->ai_family, addrs->ai_socktype, addrs->ai_protocol);
if (sfd == INVALID_SOCKET)
{
    fprintf(stderr, "Error encountered in socket call: %d\n", WSAGetLastError());
    return 1;
}
if ((connect(sfd, addrs->ai_addr, addrs->ai_addrlen)) == -1)
{
    fprintf(stderr, "Error encountered in socket call: %d\n", WSAGetLastError());
    closesocket(sfd);
    return 1;
}

注意:这只是尝试链接列表中的第一个地址,如果第一个失败 - 它只是放弃。这对于现实世界的用例来说并不理想。但是对于您的特定用例,这可能无关紧要。

非常不言自明,从您的结构中socket获取和值并创建一个套接字。它在失败时返回(这只是在 linux 上)。上的相关 linux 文档。ai_familyai_socktypeai_protocoladdrinfoINVALID_SOCKET-1socket

connect获取您刚刚从 中获取的套接字描述符socket,以及您的结构中的ai_addrai_addrlenaddrinfo以连接到服务器。它SOCKET_ERROR在失败时返回(同样,只是-1在 linux 上)。上的相关 linux 文档socket

现在所有的低级套接字恶作剧都完成了。接下来是 HTTP 恶作剧。要发出 HTTP 请求,您需要了解 HTTP 规范,您可以在此处阅读全文。

开个玩笑,这是一个更容易理解的版本——由 MDN 提供

但基本上,消息的格式是这样的——

<HTTP-VERB> <REQUEST-PATH> HTTP/<VERSION>\r\n[HEADERS-TERMINATED-WITH-CRLF]\r\n[BODY]

因此GET/blah-blah/record/record.cgi使用 HTTP 1.0 且没有标头(也没有正文,因为这是一个GET请求)的请求看起来像 -

GET /blah-blah/record/record.cgi HTTP/1.0\r\n\r\n

在继续之前,我需要提一下为什么我要展示HTTP/1.0而不是更明智的时代 - HTTP/1.1。由于并非所有服务器都1.0完全支持。两者之间的真正区别在于它1.1需要一个Host:标头,并且它允许保持连接处于活动状态 - 这对于对同一服务器的多个请求更有效。

关于Host:标头,如果您知道服务器在请求标头中期望的主机名,那就太好了!将其更改为HTTP/1.1并放入Host: hostname。在哪里hostname......嗯,主机名。据我所知,您的服务器可能只是接受192.168.1.5作为主机名,但也可能不接受。

第二部分是keep-alive连接部分。阻塞套接字会使保持活动连接变得有点复杂。如果您决定使用保持活动连接并尝试recv来自服务器的响应,当从流中没有更多内容可读取时,recv将一直阻塞直到有东西 - 因为连接仍然存在。当然有一些方法可以解决这个问题,其行为因 linux 和 windows 而异。所以我建议只将Connection标题设置为closeif using HTTP/1.1. 我认为这对于您的用例来说“足够好”。然后 HTTP 请求看起来像 -

GET /blah-blah/record/record.cgi HTTP/1.1\r\nHost: hostname\r\nConnection: close\r\n\r\n

现在,发送请求——

char const reqmsg[] = "GET /blah-blah/record/record.cgi HTTP/1.0\r\n\r\n";
if (send(sfd, reqmsg, sizeof reqmsg, 0) == SOCKET_ERROR)
{
    fprintf(stderr, "Error encountered in send call: %d\n", WSAGetLastError());
    closesocket(sfd);
    return 1;
}

send获取套接字描述符、消息和消息长度,并返回发送或SOCKET_ERROR失败时的字节数(-1在 linux 上)。linux上的相关文档send

这里有一些特殊性需要注意。send 发送的字节数可能少于完整消息的长度。这对于流套接字是正常的。send发生这种情况时,您必须通过再次调用并从离开的地方开始发送其余部分send(将返回值添加到消息字符串指针以获得连续点)。为简洁起见,省略了这一点,但对于这样一条短消息,它实际上不太可能发生。如果您有过去十年的任何机器-send将完整发送该小消息。

请不要在实际项目中依赖它。

恭喜!您已成功向服务器发送请求。现在您可以收到它的响应。响应格式是这样的——

HTTP/<VERSION> <STATUS-CODE> <STATUS-MSG>\r\n[HEADERS-TERMINATED-WITH-CRLF]\r\n[BODY]

如果您不想接收,则不必接收,数据只会位于流缓冲区中,当您关闭套接字时,它将“消失”。但这里有一个只解析状态码的小例子——

int status;
char stats[13];
if (recv(sfd, stats, 13, 0) != 13)
{
    fprintf(stderr, "Error encountered in recv call: %d\n", WSAGetLastError());
    closesocket(sfd);
    return 1;
}
sscanf(stats, "HTTP/1.0 %d ", &status);

recv, 工作方式很像send, 反之亦然。它填充您作为第二个参数传递的缓冲区,并且可以在其第三个参数中提到它将读取的最大字节数。的返回值recv是读取的字节数。linux上的相关文档recv

前面提到的特殊性在这里也存在。recv可能读取的字节数少于您在第三个参数中提到的字节数。但是对于任何半体面的互联网连接,肯定会一口气读取 13 个字节。

为什么是 13 个字节?那就是HTTP/1.0+ 一个空格 + 3 位状态码 + 一个空格的长度。我们只对此状态码感兴趣。sscanf然后将解析出状态码(假设服务器正确响应)并将其放入status.

如果您想读取任何稍微大的大小(和/或执行多个recv调用),请确保在循环中执行此操作并使用读取的字节数的返回值来适当地使用缓冲区。

在这一切之后,请务必调用freeaddrinfoonaddrsclosesocketon sfd

进一步阅读:Winsock2上的 MS 文档

在 linux 上,一切都非常相似 - 但头痛要少得多 :) -如果您正在寻找在 linux 上实现它,我建议您查看beej 的网络编程指南。

(事实上​​,这对 Windows 程序员来说也是一本好书!最新的现代功能无缝支持 ipv4 和 ipv6,无需手动和痛苦的结构填充等)

一切都将非常相似,您只需要更改标题 - 摆脱愚蠢的 Windows 特定启动,并将WSAGetLastError内部替换fprintf为常规perror.

编辑:替换复合文字 ( static struct addrinfo const hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM };)-

struct addrinfo hints;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

可以说,如果您可以访问支持 C99 及更高版本的编译器 - 请改用复合文字 - 它会更好:)


推荐阅读