使用C语言通过TCP连接端口

4

我对套接字和任何网络编程都只有99%的了解,请谅解。

我希望能够连接到本地机器(192.168.0.1)上的端口(在此情况下为2111)。然后,我计划发送和接收基本信息,但这是另一天的事情。

目前,我尝试了以下内容:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char **argv)
{
    int sd;
    int port;
    int start;
    int end;
    int rval;
    struct hostent *hostaddr;
    struct sockaddr_in servaddr;

    start = 2111;
    end   = 2112;
    for(port = start; port <= end; port++)
    {
        sd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(sd == -1)
        {
            perror("Socket()\n");
            return (errno);
        }

        memset(&servaddr, 0, sizeof(servaddr));

        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(port);

        hostaddr = gethostbyname("192.168.0.1");

        memcpy(&servaddr.sin_addr, hostaddr->h_addr, hostaddr->h_length);

        rval = connect(sd, (struct sockaddr *)&servaddr, sizeof(servaddr));
        if(rval == -1)
        {
            printf("Port %d is closed\n", port);
            close(sd);
        }
        else printf("Port %d is open\n", port);

        close(sd);
    }

    return 0;
}

然而,我的connect()调用会挂起约90秒,然后返回-1。
该设备直接连接到我Mac Mini的以太网口,制造商已经确认端口为2111或2112。
我做错了什么?另外,能用ELI5(以5岁孩子能理解的方式)解释一下吗?我最好通过例子理解。

监听该端口的进程是否在执行此程序的同一台机器上? - dari
@usr,我很想为你提供答案,但是我不知道该怎么回答。我甚至不知道你的意思是什么。 - RileyE
1
gethostbyname接受域名作为参数,并返回其相应的IP地址。你提供了一个IP地址。如果你使用Linux,一定要进行适当的错误处理。例如,使用perror方法。它能让你知道具体的错误信息。 - awatan
3
@mctylr 实际上,如果没有服务器在侦听,响应应该是立即的 ECONNREFUSED。 90秒超时更可能是完全无法到达远程服务器的故障。 - Alnitak
1
@RileyE,你认为机器为什么在192.168.0.1上?它是硬编码的吗?还是Mac上有DHCP服务器?如果是硬编码的,你如何配置Mac上的NIC? - Alnitak
显示剩余7条评论
2个回答

6
当您调用connect()连接主机时,您的计算机会发送一个SYN数据包来开始TCP连接的三次握手。从这里开始,有三种可能的情况:
  1. 如果该对等体正在监听该端口,则它会用SYN+ACK数据包进行响应,你的计算机会回复一个最终的ACK数据包,建立连接,connect()函数将成功返回。
  2. 如果该对等体未在该端口上进行监听,则它会用ICMP数据包进行响应,并指示该端口关闭,这将导致你的connect()函数几乎立即失败,并显示错误消息ECONNREFUSED(拒绝连接)。在正常情况下,这需要1个网络往返时间(RTT),通常为几十毫秒或几百毫秒。
  3. 如果你的计算机从未接收到合适的SYN+ACK TCP数据包或拒绝连接的ICMP数据包,则它会认为其原始的SYN数据包在网络中某处丢失,并尝试重新发送SYN数据包多次,直到收到其中之一或达到操作系统相关的超时时间,在此过程中,connect()函数将失败,并显示ETIMEDOUT错误代码。这通常需要1-2分钟,具体取决于操作系统及其TCP设置。

显然,你正在遇到第三种情况。这可能是由几个不同的问题引起的:

  1. 您原始的SYN数据包在网络中丢失,可能是由于故障链接、过载路由器或防火墙
  2. 对等方的SYN+ACK或ICMP响应数据包在网络中丢失,可能是由于故障链接、过载路由器或防火墙
  3. 目标地址可能无法路由/无法到达
  4. 对等方可能未能正确响应SYN+ACK或ICMP数据包

如果您直接通过以太网连接设备,则排除了#1和#2。#4可能存在,但我认为#3是最有可能的解释。

关于数据包路由的简要说明

您的计算机具有多个网络接口-以太网(有时是多个以太网接口)、Wi-Fi、环回设备、VPN隧道等。每当您创建套接字时,它必须绑定到一个或多个特定的网络接口,以便操作系统知道实际发送数据包的NIC。对于服务器的侦听套接字,通常将其绑定到所有网络接口(以侦听所有连接),但也可以将其绑定到特定的网络接口以仅在该接口上侦听。

对于客户端套接字,当您将它们连接到其他对等方时,通常不会将它们绑定到特定的接口。默认情况下,您的计算机使用其内部路由表以及目标IP地址来确定要使用哪个网络接口。例如,如果您有一个网关机器,其中两个NIC之一连接到具有IP 54.x.y.z的公共Internet,另一个连接到具有IP 192.168.1.1的内部私有网络,则该机器很可能具有路由表,指示“对于目标为192.168.0.0/16的数据包,请使用NIC 2,对于所有其他数据包,请使用NIC 1”。如果您想绕过路由表,可以在调用connect()之前,在套接字上调用bind(),将套接字绑定到您想要的网络接口。
将所有内容结合起来,这对您意味着什么?首先,请确保192.168.0.1实际上是您应该连接到的正确目标地址。如何确定该地址?您的计算机是否充当DHCP服务器,将该地址分配给其他主机?该主机是否使用静态IP配置?

接下来,请确保您的路由表是正确的。如果另一台机器分配了静态IP地址,那么您的Mac可能不知道如何路由到该目标,并且可能正在尝试通过错误的接口进行路由。您可以使用route(8)实用程序在Mac OS X上手动调整路由,但这些在每次重新启动时都会重置; 此博客文章展示了使用启动项自动添加新路由的示例。您需要使用与连接到目标主机的以太网接口相关联的IP地址。

另外,除了使用路由表之外,您还可以在connect()之前在套接字上调用bind()以绑定到要使用的接口的本地地址,但是这对于其他程序而言可能无效,除非它们也提供了该功能。例如,curl(1)实用程序允许您传递--interface <name>命令行标志以将其指向绑定到特定接口。


1

基本上,connect() 失败了(检查 errno 以获取失败原因)。

您可以考虑实现某种连接超时。要做到这一点,将套接字设置为非阻塞模式。然后调用 connect(),再使用 select() 等待带有超时的响应。

SPOILER 适用于 Linux 的 ConnectWithTimeout() 示例


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接