# 网络，第 4 部分：构建一个简单的 TCP 服务器

> 原文：<https://github.com/angrave/SystemProgramming/wiki/Networking%2C-Part-4%3A-Building-a-simple-TCP-Server>

## 什么是`htons`以及何时使用？

整数可以首先表示最低有效字节或最高有效字节。只要机器本身内部一致，任何一种方法都是合理的。对于网络通信，我们需要对商定的格式进行标准化。

`htons(xyz)`以网络字节顺序返回 16 位无符号整数'short'值 xyz。 `htonl(xyz)`以网络字节顺序返回 32 位无符号整数'long'值 xyz。

这些功能被读作“主机到网络”;反函数（ntohs，ntohl）将网络有序字节值转换为主机有序排序。那么，是主机订购 little-endian 还是 big-endian？答案是 - 这取决于你的机器！它取决于运行代码的主机的实际体系结构。如果体系结构恰好与网络排序相同，那么这些函数的结果就是参数。对于 x86 机器，主机和网络订购 _ 与 _ 不同。

简介：每当您读取或写入低级 C 网络结构（例如端口和地址信息）时，请记住使用上述功能以确保正确转换为/从数据库格式转换。否则显示或指定的值可能不正确。

## 用于创建服务器的“4 大”网络呼叫是什么？

创建 TCP 服务器所需的四个系统调用是：`socket`，`bind` `listen`和`accept`。每个都有特定的目的，应按上述顺序调用

端口信息（由 bind 使用）可以手动设置（许多较旧的仅使用 IPv4 的 C 代码示例执行此操作），或使用`getaddrinfo`创建

我们以后也会看到 setsockopt 的例子。

## 调用`socket`的目的是什么？

为网络通信创建端点。一个新的插座本身并不是特别有用;虽然我们已经指定了数据包或基于流的连接，但它并未绑定到特定的网络接口或端口。相反，套接字返回一个网络描述符，可以用于以后调用 bind，listen 和 accept。

## 调用`bind`的目的是什么

`bind`调用将抽象套接字与实际网络接口和端口相关联。可以在 TCP 客户端上调用 bind，但是通常不需要指定传出端口。

## 调用`listen`的目的是什么

`listen`调用指定传入的未处理连接数的队列大小，即尚未通过`accept`分配网络描述符的队列大小。高性能服务器的典型值为 128 或更多。

## 为什么服务器套接字被动？

服务器套接字不会主动尝试连接到另一台主机;相反，他们等待传入的连接。此外，当对等设备断开连接时，服务器套接字不会关闭。相反，当远程客户端连接时，它会立即被撞到未使用的端口号以供将来通信。

## 调用`accept`的目的是什么

初始化服务器套接字后，服务器调用`accept`以等待新连接。与`socket` `bind`和`listen`不同，此调用将被阻止。即，如果没有新连接，则此呼叫将阻止，并且仅在新客户端连接时返回。

注意`accept`调用返回一个新的文件描述符。此文件描述符特定于特定客户端。将原始服务器套接字描述符用于服务器 I / O 然后想知道为什么网络代码失败是常见的编程错误。

## 创建 TCP 服务器的难点是什么？

*   使用被动服务器套接字的套接字描述符（如上所述）
*   未指定 getaddrinfo 的 SOCK_STREAM 要求
*   无法重新使用现有端口。
*   不初始化未使用的 struct 条目
*   如果当前正在使用端口，则`bind`调用将失败

注意，端口是每台机器 - 不是每个进程或每个用户。换句话说，当另一个进程正在使用该端口时，您无法使用端口 1234。更糟糕的是，端口在进程完成后默认为“绑定”。

## 服务器代码示例

一个工作简单的服务器示例如下所示。请注意，此示例不完整 - 例如，它不会关闭套接字描述符，也不会释放由`getaddrinfo`创建的内存

```c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    int s;
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct addrinfo hints, *result;
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;

    s = getaddrinfo(NULL, "1234", &hints, &result);
    if (s != 0) {
            fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
            exit(1);
    }

    if (bind(sock_fd, result->ai_addr, result->ai_addrlen) != 0) {
        perror("bind()");
        exit(1);
    }

    if (listen(sock_fd, 10) != 0) {
        perror("listen()");
        exit(1);
    }

    struct sockaddr_in *result_addr = (struct sockaddr_in *) result->ai_addr;
    printf("Listening on file descriptor %d, port %d\n", sock_fd, ntohs(result_addr->sin_port));

    printf("Waiting for connection...\n");
    int client_fd = accept(sock_fd, NULL, NULL);
    printf("Connection made: client_fd=%d\n", client_fd);

    char buffer[1000];
    int len = read(client_fd, buffer, sizeof(buffer) - 1);
    buffer[len] = '\0';

    printf("Read %d chars\n", len);
    printf("===\n");
    printf("%s\n", buffer);

    return 0;
}
```

## 为什么我的服务器无法重新使用该端口？

默认情况下，套接字关闭时不会立即释放端口。相反，端口进入“TIMED-WAIT”状态。这可能会在开发过程中导致严重的混淆，因为超时可能会使有效的网络代码看起来失败。

为了能够立即重新使用端口，请在绑定到端口之前指定`SO_REUSEPORT`。

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

bind(....
```

这是[对`SO_REUSEPORT`](http://stackoverflow.com/questions/14388706/socket-options-so-reuseaddr-and-so-reuseport-how-do-they-differ-do-they-mean-t) 的扩展 stackoverflow 介绍性讨论。