简单 TCP 服务器和客户端 (改进版)

介绍

对比 简单的 TCP 服务器和客户端, 本文中的服务器和客户端程序改进了以下几点:

  1. 使用 inet_ntop() 函数, 使得服务器程序可以正确处理 IPv4 和 IPv6 地址.

  2. 使用 getaddrinfo() 函数, 使得服务器程序可以正确处理 IPv4 和 IPv6 地址.

  3. 使用 fork() 函数, 使得服务器程序可以同时处理多个客户端的连接请求;

  4. 使用 waitpid() 函数, 使得服务器程序可以正确处理子进程的退出.

服务端代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>

#define PORT "43866"    /* 端口号 */
#define BACKLOG 10      /* 最大连接请求数 */
#define MAXDATASIZE 100 /* 最大数据传输量 */

/* 信号处理函数, 用于处理子进程退出 */
void sigchld_handler(int s)
{
    /* 因为 waitpid() 可能会改变 errno 的值,所以先保存,然后恢复 */
    int saved_errno = errno;

    /* 等待所有子进程退出 */
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;

    /* 恢复 errno */
    errno = saved_errno;
}

/* 获取 sockaddr 结构体的 IP 地址 */
void *get_in_addr(struct sockaddr *sa)
{
    /* 判断地址族 */
    if (sa->sa_family == AF_INET)
    {
        /* 返回 IPv4 地址 */
        return &(((struct sockaddr_in *)sa)->sin_addr);
    }
    /* 返回 IPv6 地址 */
    return &(((struct sockaddr_in6 *)sa)->sin6_addr);
}

/* 获取 sockaddr 结构体的端口号 */
int get_in_port(struct sockaddr *sa)
{
    /* 判断地址族 */
    if (sa->sa_family == AF_INET)
    {
        /* 返回 IPv4 端口号 */
        return (((struct sockaddr_in *)sa)->sin_port);
    }
    /* 返回 IPv6 端口号 */
    return (((struct sockaddr_in6 *)sa)->sin6_port);
}

/* 主函数 */
int main(int argc, char *argv[])
{
    int sockfd, new_fd;                   /* sockfd:监听套接字, new_fd:数据传输套接字 */
    struct addrinfo hints, *servinfo, *p; /* hints: 用于设置 addrinfo 结构体, servinfo: 存放地址信息, p: addrinfo 指针,用于遍历 servinfo */
    struct sockaddr_storage their_addr;   /* 用于存放客户端 IP 地址 和 端口号 信息 */
    socklen_t sin_size;                   /* 用于 accept() */
    struct sigaction sa;                  /* 用于 sigaction() */
    int yes = 1;                          /* 用于设置 setsockopt() 函数 */
    int rv;                               /* 用于存放 getaddrinfo() 函数的返回值 */
    char s[INET6_ADDRSTRLEN];             /* 用于存放 inet_ntop() 函数返回的地址信息的字符串 */
    int numbytes_send;                    /* 用于存放 send() 函数的返回值 */
    int numbytes_recv;                    /* 用于存放 recv() 函数的返回值 */
    char buf[MAXDATASIZE];                /* 用于存放接收到的数据 */

    /* 初始化 hints 结构体 */
    memset(&hints, 0, sizeof hints);

    /* 设置 hints 结构体 */
    hints.ai_family = AF_UNSPEC;     /* 不指定 IPv4 或 IPv6 */
    hints.ai_socktype = SOCK_STREAM; /* TCP stream sockets */
    hints.ai_flags = AI_PASSIVE;     /* 使用本机 IP */

    /* 获取地址信息 */
    if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0)
    {
        /* 打印错误信息 */
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    /* 循环 servinfo 结构体链表 */
    for (p = servinfo; p != NULL; p = p->ai_next)
    {
        /* 创建套接字 */
        if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
        {
            /* 打印错误信息 */
            perror("server: socket");
            continue;
        }

        /* 设置套接字选项 */
        // SO_REUSEADDR 选项用于允许重用本地地址和端口
        if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)
        {
            /* 打印错误信息 */
            perror("setsockopt");
            exit(1);
        }

        /* 绑定套接字 */
        if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1)
        {
            /* 连接服务器失败 */
            close(sockfd);
            perror("server: bind");
            continue;
        }

        break;
    }

    /* 释放结果链表 */
    freeaddrinfo(servinfo);

    /* 检查是否成功绑定 */
    if (p == NULL)
    {
        fprintf(stderr, "server: failed to bind\n");
        exit(1);
    }

    /* 监听套接字 */
    if (listen(sockfd, BACKLOG) == -1)
    {
        perror("listen");
        exit(1);
    }

    /* 信号处理函数 */
    sa.sa_handler = sigchld_handler; /* 处理所有僵尸进程 */
    sigemptyset(&sa.sa_mask);        /* 屏蔽所有信号 */
    sa.sa_flags = SA_RESTART;        /* 重启被中断的系统调用 */
    if (sigaction(SIGCHLD, &sa, NULL) == -1)
    {
        /* 打印错误信息 */
        perror("sigaction");
        exit(1);
    }

    printf("server: waiting for connections...\n");

    /* 主循环 for accept() */
    while (1)
    {
        sin_size = sizeof their_addr;
        /* 接受连接请求 */
        new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
        if (new_fd == -1)
        { /* 接受连接请求失败 */
            perror("accept");
            continue;
        }

        /* 将 IP 地址转换为字符串 */
        inet_ntop(their_addr.ss_family, get_in_addr((struct sockaddr *)&their_addr), s, sizeof s);
        printf("server: got connection from %s, port %d, socketfd %d\n", s, ntohs(get_in_port((struct sockaddr *)&their_addr)), new_fd);

        /* 创建子进程来处理新连接请求 */
        if (!fork())
        {
            /* 子进程不需要监听套接字 */
            close(sockfd);
            /* 子进程处理数据传输 */
            /* 构建传递信息 */
            char *msg = "Hello world from server!";
            int msg_len = strlen(msg);
            /* 发送信息到客户端 */
            if ((numbytes_send = send(new_fd, msg, msg_len, 0)) == -1)
                perror("send");
            /* 打印发送信息 */
            printf("server: sent message: %s (%d bytes) to %s, port %d, sockfd %d\n", msg, numbytes_send, s, ntohs(get_in_port((struct sockaddr *)&their_addr)), new_fd);
            /* 接收客户端信息 */
            if ((numbytes_recv = recv(new_fd, buf, MAXDATASIZE - 1, 0)) == -1)
            {
                perror("recv");
                exit(1);
            }
            buf[numbytes_recv] = '\0';
            printf("server: received message: %s (%d bytes) from %s, port %d, sockfd %d\n", buf, numbytes_recv, s, ntohs(get_in_port((struct sockaddr *)&their_addr)), new_fd);
            /* 关闭数据传输套接字 */
            close(new_fd);
            printf("\n");
            /* 退出子进程 */
            exit(0);
        }
        /* 父进程不需要数据传输套接字 */
        close(new_fd);
    }

    return 0;
}

/* References: Page 31-33, Beej's Guide to Network Programming Using Internet Sockets */

客户端代码

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

#define PORT "43866"    /* 端口号 */
#define MAXDATASIZE 100 /* 每次最大数据传输量 */

/* 获取 sockaddr,IPv4 或 IPv6: */
void *get_in_addr(struct sockaddr *sa)
{
    /* 判断地址族 */
    if (sa->sa_family == AF_INET)
    {
        /* 返回 IPv4 地址 */
        return &(((struct sockaddr_in *)sa)->sin_addr);
    }
    /* 返回 IPv6 地址 */
    return &(((struct sockaddr_in6 *)sa)->sin6_addr);
}

int main(int argc, char *argv[])
{
    int sockfd;                           /* sockfd: 用于存放 socket() 函数的返回值 */
    char buf[MAXDATASIZE];                /* 缓冲区,用于存放接收到的数据 */
    struct addrinfo hints, *servinfo, *p; /* hints: 用于设置 addrinfo 结构体, servinfo: 存放地址信息, p: addrinfo 指针,用于遍历 servinfo */
    int rv;                               /* 用于存放 getaddrinfo() 函数的返回值 */
    char s[INET6_ADDRSTRLEN];             /* 用于存放 inet_ntop() 函数返回的地址信息的字符串 */
    int numbytes_recv;                    /* 用于存放 recv() 函数的返回值 */
    int numbytes_send;                    /* 用于存放 send() 函数的返回值 */

    /* 检查参数个数 */
    if (argc != 2)
    {
        fprintf(stderr, "usage: ./client hostname\n");
        exit(1);
    }

    /* 初始化 hints 结构体 */
    memset(&hints, 0, sizeof hints);

    /* 设置 hints 结构体 */
    hints.ai_family = AF_UNSPEC;     /* 不指定 IPv4 或 IPv6 */
    hints.ai_socktype = SOCK_STREAM; /* TCP stream sockets */

    /* 获取地址信息 */
    if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0)
    {
        /* 打印错误信息 */
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    /* 遍历 servinfo 结构体链表 */
    for (p = servinfo; p != NULL; p = p->ai_next)
    {
        /* 创建套接字 */
        if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
        {
            /* 打印错误信息 */
            perror("client: socket");
            continue;
        }

        /* 连接服务器 */
        if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1)
        {
            /* 连接服务器失败 */
            close(sockfd);
            perror("client: connect");
            continue;
        }

        break;
    }

    /* 遍历完 servinfo 结构体,仍然没有连接成功 */
    if (p == NULL)
    {
        fprintf(stderr, "client: failed to connect to server\n"); /* 发送数据到服务器 */
        return 2;
    }

    /* 将地址信息转换为字符串 */
    inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr), s, sizeof s);

    /* 打印连接的服务器的地址信息 */
    printf("client: connecting to %s port %s\n", s, PORT);

    /* 释放 servinfo 结构体 */
    freeaddrinfo(servinfo);

    /* 接收数据 */
    if ((numbytes_recv = recv(sockfd, buf, MAXDATASIZE - 1, 0)) == -1)
    {
        /* 接收数据失败 */
        perror("recv");
        exit(1);
    }

    /* 添加字符串结束符 */
    buf[numbytes_recv] = '\0';

    /* 打印接收到的数据 */
    printf("client: received '%s' from server.\n", buf);

    /* 构建待发送的数据 */
    char *msg = "Hello world from client!";
    int msg_len = strlen(msg);

    /* 发送数据到服务器 */
    if ((numbytes_send = send(sockfd, msg, msg_len, 0)) == -1)
    {
        /* 发送数据失败 */
        perror("send");
    }
    printf("client: sent '%s' (%d bytes) to server.\n", msg, numbytes_send);

    /* 关闭套接字 */
    close(sockfd);

    printf("client: closed socketfd %d.\n", sockfd);
    return 0;
}

/* References: Page 31-33, Beej's Guide to Network Programming Using Internet Sockets */

编译

gcc server.c -o server
gcc client.c -o client

运行与输出

$ ./server
server: waiting for connections...
server: got connection from 127.0.0.1, port 56836, socketfd 4
server: sent message: Hello world from server! (24 bytes) to 127.0.0.1, port 56836, sockfd 4
server: received message: Hello world from client! (24 bytes) from 127.0.0.1, port 56836, sockfd 4
$ ./client localhost
client: connecting to 
client: received 'Hello world from server!' from server.
client: sent 'Hello world from client!' (24 bytes) to server.
client: closed socketfd 3

如果在服务端运行前运行客户端, 会打印如下错误信息:

$ ./client localhost
client: connect: Connection refused
client: failed to connect to server

参考资料

A Simple Stream Server - Beej’s Guide to Network Programming

A Simple Stream Client - Beej’s Guide to Network Programming


最后修改于 2023-02-03


感谢您的支持 :D