# 网络，第 5 部分：关闭端口，重用端口和其他技巧

> 原文：<https://github.com/angrave/SystemProgramming/wiki/Networking%2C-Part-5%3A-Shutting-down-ports%2C-reusing-ports-and-other-tricks>

## 关闭和关闭有什么区别？

当您不再需要从套接字读取更多数据，写入更多数据或完成这两项操作时，请使用`shutdown`调用。当您关闭套接字以进一步写入（或读取）时，该信息也会发送到连接的另一端。例如，如果您关闭套接字以便在服务器端进一步写入，那么片刻之后，阻塞的`read`调用可能会返回 0 以指示不再需要更多字节。

当您的进程不再需要套接字文件描述符时，请使用`close`。

如果在创建套接字文件描述符后`fork` -ed，则所有进程都需要关闭套接字才能重新使用套接字资源。如果关闭套接字以进一步读取，那么所有进程都会受到影响，因为您已经更改了套接字，而不仅仅是文件描述符。

编写好的代码会在调用`close`之前将`shutdown`作为套接字。

## 当我重新运行我的服务器代码时，它不起作用！为什么？

默认情况下，在套接字关闭后，端口进入超时状态，在此期间无法重新使用（“绑定到新套接字”）。

通过在绑定到端口之前设置套接字选项 REUSEPORT 可以禁用此行为：

```c
    int optval = 1;
    setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

    bind(sock_fd, ...);
```

## TCP 客户端可以绑定到特定端口吗？

是!实际上，传出的 TCP 连接会自动绑定到客户端上未使用的端口。通常不必在客户端上明确设置端口，因为系统将智能地在合理的接口上找到不可用的端口（例如，如果当前通过 WiFi 连接连接，则为无线卡）。但是，如果您需要专门选择特定的以太网卡，或者防火墙仅允许来自特定范围的端口值的传出连接，则它可能很有用。

要显式绑定到以太网接口和端口，请在`connect`之前调用`bind`

## 谁连接到我的服务器？

`accept`系统调用可以选择通过传入 sockaddr 结构来提供有关远程客户端的信息。不同的协议具有`struct sockaddr`的不同变体，它们具有不同的大小。最简单的结构是`sockaddr_storage`，它足够大以代表所有可能类型的 sockaddr。请注意，C 没有任何继承模型。因此，我们需要将结构显式地转换为'base type'结构 sockaddr。

```c
    struct sockaddr_storage clientaddr;
    socklen_t clientaddrsize = sizeof(clientaddr);
    int client_id = accept(passive_socket,
            (struct sockaddr *) &clientaddr,
             &clientaddrsize);
```

我们已经看到`getaddrinfo`可以构建 addrinfo 条目的链接列表（并且每个条目中的每一个都可以包括套接字配置数据）。如果我们想将套接字数据转换为 IP 和端口地址怎么办？输入`getnameinfo`，可用于将本地或远程套接字信息转换为域名或数字 IP。类似地，端口号可以表示为服务名称（例如，端口 80 的“http”）。在下面的示例中，我们请求客​​户端 IP 地址和客户端端口号的数字版本。

```c
    socklen_t clientaddrsize = sizeof(clientaddr);
    int client_id = accept(sock_id, (struct sockaddr *) &clientaddr, &clientaddrsize);
    char host[256], port[256];
    getnameinfo((struct sockaddr *) &clientaddr,
          clientaddrsize, host, sizeof(host), port, sizeof(port),
          NI_NUMERICHOST | NI_NUMERICSERV);
```

Todo：讨论 NI_MAXHOST 和 NI_MAXSERV 以及 NI_NUMERICHOST

## getnameinfo 示例：我的 IP 地址是什么？

要获取当前计算机的 IP 地址的链接列表，请使用`getifaddrs`，它将返回 IPv4 和 IPv6 IP 地址的链接列表（以及可能还有其他接口）。我们可以检查每个条目并使用`getnameinfo`打印主机的 IP 地址。 ifaddrs 结构包括族，但不包括结构的大小。因此，我们需要手动确定基于系列的结构大小（IPv4 v IPv6）

```c
 (family == AF_INET) ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6)
```

完整的代码如下所示。

```c
    int required_family = AF_INET; // Change to AF_INET6 for IPv6
    struct ifaddrs *myaddrs, *ifa;
    getifaddrs(&myaddrs);
    char host[256], port[256];
    for (ifa = myaddrs; ifa != NULL; ifa = ifa->ifa_next) {
        int family = ifa->ifa_addr->sa_family;
        if (family == required_family && ifa->ifa_addr) {
            if (0 == getnameinfo(ifa->ifa_addr,
                                (family == AF_INET) ? sizeof(struct sockaddr_in) :
                                sizeof(struct sockaddr_in6),
                                host, sizeof(host), port, sizeof(port)
                                 , NI_NUMERICHOST | NI_NUMERICSERV  ))
                puts(host);
            }
        }
```

## 什么是我机器的 IP 地址（shell 版本）

答案：使用`ifconfig`（或 Windows 的 ipconfig）但是这个命令会为每个接口生成大量输出，所以我们可以使用 grep 过滤输出

```
ifconfig | grep inet

Example output:
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
    inet 127.0.0.1 netmask 0xff000000 
    inet6 ::1 prefixlen 128 
    inet6 fe80::7256:81ff:fe9a:9141%en1 prefixlen 64 scopeid 0x5 
    inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255 
```