1

accept() 被定义为总是创建另一个文件描述符来接受来自客户端的新连接,但是如果事先知道我们只会接受一个客户端和一个连接,为什么还要创建一个新的文件描述符呢?在任何定义的标准中是否有任何描述为什么会出现这种情况?

4

6 回答 6

3

在设计 API 时,我认为通用性是有价值的。为什么有 2 个 API,一个用于接受潜在的多个连接,一个用于使用更少的文件描述符?后一种情况似乎没有足够高的优先级来证明一个全新的系统调用是合理的,因为我们今天拥有的 API 可以做到,并且您可以使用它来实现您想要的行为。

另一方面,WindowsAcceptEx允许您重新使用以前表示其他不相关的以前连接的套接字的以前的套接字句柄。我相信这是为了避免在断开连接后再次进入内核关闭套接字对性能的影响。不完全是你所描述的,但模糊相似。(虽然意味着扩大而不是缩小。)

更新:一个月后,我觉得你为此创造了一个赏金有点奇怪。我认为答案很明确——当前的界面可以满足您的要求,而且真的没有动力为您的边缘案例添加新界面,更不用说标准化了。使用当前接口,您可以在成功close后使用原始套接字accept,它不会伤害任何人。

于 2012-06-26T06:55:30.300 回答
1

RFC 793中描述的 TCP 协议描述了术语socketconnection套接字是 IP 地址和端口号对。连接是套接字。从这个意义上说,同一个套接字可以用于多个连接。正是在这个意义上,被传递给正在被使用。由于一个套接字可用于多个连接,并且传递的 to表示该套接字,API 会创建一个新的来表示该连接socketaccept()socketaccept()socket

如果您只是想要一种简单的方法来确保为socketaccept()创建的套接字与您用来进行accept()调用的套接字相同,那么请使用包装器 FTW:

int accept_one (int accept_sock, struct sockaddr *addr, socklen_t *addrlen) {
    int sock = accept(accept_sock, addr, addrlen);
    if (sock >= 0) {
        dup2(sock, accept_sock);
        close(sock);
        sock = accept_sock;
    }
    return sock;
}

如果您想要一种客户端和服务器相互连接的方式,而不只是socket在每一侧创建一个,那么确实存在这样的 API。API 是connect(),当你实现同时打开时它就成功了。

static struct sockaddr_in server_addr;
static struct sockaddr_in client_addr;

void init_addr (struct sockaddr_in *addr, short port) {
    struct sockaddr_in tmp = {
        .sin_family = AF_INET, .sin_port = htons(port),
        .sin_addr = { htonl(INADDR_LOOPBACK) } };
    *addr = tmp;
}

void connect_accept (int sock,
                     struct sockaddr_in *from, struct sockaddr_in *to) {
    const int one = 1;
    int r;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    bind(sock, (struct sockaddr *)from, sizeof(*from));
    do r = connect(sock, (struct sockaddr *)to, sizeof(*to)); while (r != 0);
}

void do_peer (char *who, const char *msg, size_t len,
              struct sockaddr_in *from, struct sockaddr_in *to) {
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    connect_accept(sock, from, to);
    write(sock, msg, len-1);
    shutdown(sock, SHUT_WR);
    char buf[256];
    int r = read(sock, buf, sizeof(buf));
    close(sock);
    if (r > 0) printf("%s received: %.*s%s", who, r, buf,
                      buf[r-1] == '\n' ? "" : "...\n");
    else if (r < 0) perror("read");
}

void do_client () {
    const char msg[] = "client says hi\n";
    do_peer("client", msg, sizeof(msg), &client_addr, &server_addr);
}

void do_server () {
    const char msg[] = "server says hi\n";
    do_peer("server", msg, sizeof(msg), &server_addr, &client_addr);
}

int main () {
    init_addr(&server_addr, 4321);
    init_addr(&client_addr, 4322);
    pid_t p = fork();
    switch (p) {
    case 0:  do_client(); break;
    case -1: perror("fork"); exit(EXIT_FAILURE);
    default: do_server(); waitpid(p, 0, 0);
    }
    return 0;
}

相反,如果您担心性能问题,我相信这些担心是错误的。使用 TCP 协议,您已经必须在客户端和服务器之间的网络上等待至少一个完整的往返行程,因此处理另一个套接字的额外开销可以忽略不计。如果客户端和服务器在同一台机器上,您可能会关心这种开销,但即便如此,如果连接的寿命很短,这只是一个问题。如果连接如此短暂,那么重新设计您的解决方案以使用更便宜的通信介质(例如,共享内存)或对您的数据应用框架并使用持久连接可能会更好。

于 2012-07-31T00:26:37.657 回答
0

因为它不是必需的。如果您只有一个客户,您只需执行一次操作;您有大量的文件描述符可供使用;与网络开销相比,“开销”非常小。作为 API 设计人员,您想要“优化”的情况是当您拥有数千个客户端时。

于 2012-06-26T23:04:45.903 回答
0

在listen返回的socket和accept返回的socket描述符之间唯一不同的是新socket处于ESTABILISHED状态而不是LISTEN状态。所以你可以重新使用调用listen函数后创建的socket接受其他连接。

于 2012-07-27T01:17:17.623 回答
0

因为 accept() 旨在接受新客户。

它需要三件事,通用套接字描述符必须绑定到特定端口号以在该端口号上服务,以及存储客户端信息的结构和另一个用于存储客户端大小的 int 值。

它返回一个 new_socket_descriptor 用于为服务器接受的特定客户端提供服务。

第一个参数是用于接受客户端的套接字描述符。对于并发服务器,它始终用于接受客户端连接。因此它不应被任何 accept() 调用修改。

因此 accept() 返回的新套接字描述符为新连接的客户端提供服务。

服务器套接字描述符(第一个参数)绑定到服务器属性。服务器属性始终设计为固定类型,即其端口号、连接类型、协议系列都是固定的。因此,一次又一次地使用相同的文件描述符。

另一点是这些属性用于过滤为该特定服务器建立的客户端连接。

对于客户端,每个客户端使用的每个客户端的不同最小 ip 地址的信息都是唯一的,并且这些属性绑定到新的文件描述符,因此总是由 accept() 函数成功返回一个新的文件描述符。

笔记:-

也就是说,您需要一个文件描述符才能让客户端接受,并且取决于您想要接受/服务的最大客户端数量,使用那么多文件描述符来服务客户端。

于 2012-07-27T10:14:19.197 回答
0

答案是您的特定连接示例在当前 API 中处理的,并且从一开始就被设计到 API 的用例中。关于如何处理单套接字情况的解释在于最初发明 BSD 套接字接口时套接字程序被设计为工作的方式。

套接字 API 旨在始终能够接受连接。基本原则是,当连接到达时,程序应该对是否接受连接有最终决定权。但是,应用程序在做出此决定时也绝不能错过任何连接。因此,API 被设计为仅是并行的,accept()并被指定为从 中返回不同的套接字listen(),以便listen()在应用程序决定刚刚收到的连接请求时可以继续侦听进一步的连接请求。这是一个基本的设计决策,没有记录在任何地方;只是假设套接字程序必须以这种方式工作才能有用。

在线程被发明之前,在类 Unix 系统上实现套接字服务器所需的并行性依赖于fork(). 一个新的连接被接受,程序会使用 将自己分成两个相同的副本fork(),然后一个副本将处理新连接,而原始副本继续侦听传入的连接尝试。在fork()模型中,即使accept()返回一个新的文件句柄,也支持仅处理一个连接的用例,并且仅通过让程序的“侦听”副本退出而第二个“接受”副本处理单个连接来实现。

以下伪代码显示了这一点:

fd = socket();

listen(fd, 1);  /* allow 1 unanswered connection in the backlog */

switch (fork())
{
    case  0: break;     /* child process; handle connection */
    case -1: exit (1);  /* error. exit anyway. */
    default: exit (0);  /* parent process; exit as only one connection needed */
}

/* if we get here our single connection can be accepted and handled.
 */
accept_fd = accept(fd);

这种编程范式意味着无论服务器接受单个连接,还是停留在处理多个连接的循环中,两种情况下的代码实际上都是相同的。现在我们有线程而不是fork(). 但是,由于该范例至今仍然存在,因此从来没有必要更改或升级套接字 API。

于 2012-07-30T09:25:40.053 回答