TCP实现公网服务器和内网客户端一对多访问(C语言实现)

V1.0 2024年6月5日 发布于博客园

理论

服务器端先运行, 能够接收来自任何地方的多个客户端发起的指向特定端口(这里是50002)的TCP请求, 并和客端建立稳定的TCP连接. 没有连接请求时等待, 有连接后先来后到的原则, 依次服务, 能够相互通信.

当客户端结束请求后, 自动接通第二个客户端, 为其服务. (不足: 由于从终端读取要发送的数据会阻塞, 故而上一个客户端结束后要手动输入任意字符解除阻塞后才能自动接通下一个, 在下一个版本中修改)

客户端: 向服务器发起TCP连接请求, 并和服务器相互通信.

image

由于TCP有3次握手和4次挥手┏(^0^)┛, 且TCP连接的网路会被运营商的NAT保留数小时甚至数天, 故而不需要打洞.

但只能是内网客户端与公网服务端相互通信!

代码

服务器端

/**
 * @file name : tcp_server.c
 * @brief     : TCP服务器IP, 响应客户端 端口号  与客户端建立链接
 * @author    : RISE_AND_GRIND@163.com
 * @date      : 2024年6月5日
 * @version   : 1.0
 * @note      : 编译命令 cc tcp_server.c -o tcp_server.out -pthread
 * // 运行服务器可执行文件 ./xxx 要监听的端口
 *              运行:./tcp_server.out  50001
 *              输入 exit 退出服务器端
 * 待解决: 当客户端结束后, 服务器端需要发送一个任意信息(无效), 接通下一个客户端
 * CopyRight (c)  2023-2024   RISE_AND_GRIND@163.com   All Right Reseverd
 */
#include <stdio.h>
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>

#define BUF_SIZE 1024 // 缓冲区大小(字节)

// 客户端网路信息结构体
typedef struct
{
    int sock_fd;                      // 套接字文件描述符
    struct sockaddr_in socket_addr;   // 定义套接字所需的地址信息结构体
    socklen_t addr_len;               // 目标地址的长度
    char receive_msgBuffer[BUF_SIZE]; // 发送给客户端的保活包, 表示我是服务器, C<---NET--S
} ClientArgs_t;
int client_live_flag = -1;      //-1默认 0表示退出 1表示在线
volatile sig_atomic_t stop = 0; // 添加易变的信号量
// 信号处理程序,当接收到 SIGINT 信号时(通常是按下 Ctrl+C),它将 stop 变量设置为 1。
void handle_sigint(int sig)
{
    stop = 1;
}

/**
 * @name      ReceivedFromClient
 * @brief     接收线程函数, 用于处理C-->S的信息
 * @param     client_args 线程例程参数, 传入保活包的网络信息
 * @note
 */
void *ReceivedFromClient(void *client_args)
{
    // 用于传入的是void* 需要强转才能正确指向
    ClientArgs_t *ka_client_args = (ClientArgs_t *)client_args;
    while (!stop) // 信号处理,以便更优雅地退出程序。
    {
        ssize_t bytes_read = read(ka_client_args->sock_fd, ka_client_args->receive_msgBuffer, sizeof(ka_client_args->receive_msgBuffer));
        if (bytes_read > 0)
        {
            printf("recv from [%s], data is = %s\n", inet_ntoa(ka_client_args->socket_addr.sin_addr), ka_client_args->receive_msgBuffer);
            bzero(ka_client_args->receive_msgBuffer, sizeof(ka_client_args->receive_msgBuffer));
        }
        else if (bytes_read == 0)
        {
            printf("客户端断开连接\n");
            break;
        }
        else
        {
            perror("读取客户端发送的数据错误");
            break;
        }
    }
    close(ka_client_args->sock_fd); // 关闭与该客户端的链接
    free(ka_client_args);           // 释放空间
    client_live_flag = 0;           // 客户端退出
    pthread_exit(NULL);             // 退出子线程
}

int main(int argc, char const *argv[])
{

    // 检查参数有效性
    if (argc != 2)
    {
        fprintf(stderr, "请正确输入端口号: %s <port>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    signal(SIGINT, handle_sigint); // 捕捉信号
    /************第一步: 打开套接字, 得到套接字描述符************/
    // 1.创建TCP套接字
    int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (tcp_socket == -1)
    {
        fprintf(stderr, "tcp套接字打开错误, errno:%d, %s\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }
    /************END*************/

    /************第二步: 将套接字描述符与端口绑定************/

    // 服务器端的IP信息结构体
    struct sockaddr_in server_addr;
    // 配置服务器地址信息 接受来自任何地方的数据 包有效 但只解析输入端口范围的包
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;                // 协议族,是固定的
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 目标地址  INADDR_ANY 这个宏是一个整数,所以需要使用htonl转换为网络字节序
    server_addr.sin_port = htons(atoi(argv[1]));     // 目标端口,必须转换为网络字节序

    // 绑定socket到指定端口
    if (bind(tcp_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        fprintf(stderr, "将服务器套接字文件描述符绑定IP失败, errno:%d, %s\n", errno, strerror(errno));
        close(tcp_socket);
        exit(EXIT_FAILURE);
    }
    /************END*************/

    /************第三步: 设置监听信息************/
    // 3.设置监听  队列最大容量是5
    if (listen(tcp_socket, 5) < 0)
    {
        fprintf(stderr, "设置监听失败, errno:%d, %s\n", errno, strerror(errno));
        close(tcp_socket);
        exit(EXIT_FAILURE);
    }
    printf("服务器已经运行, 开始监听中...\n");
    /************END*************/

    while (!stop)
    {
        /************第四步: 等待连接************/
        // 4.等待接受客户端的连接请求, 阻塞等待有一个C请求连接
        struct sockaddr_in client;
        socklen_t client_len = sizeof(client);
        printf("从队列出取出一个请求或等待新的客户端连接\n");
        int connect_fd = accept(tcp_socket, (struct sockaddr *)&client, &client_len); // 会阻塞
        client_live_flag = 1;
        printf("已经从队列出取出一个请求, 连接成功\n");
        // 此时得到该客户端的新套接字, 使用子线程用于接收客户端信息, 主线程发送信息给客户端
        if (connect_fd < 0)
        {
            if (errno == EINTR && stop)
            {
                break;
            }
            fprintf(stderr, "接受连接失败, 队列异常, errno:%d, %s\n", errno, strerror(errno));
            continue;
        }

        /************END*************/
        /*********************创建接收线程********************/
        // 子线程专属 客户端信息结构体
        ClientArgs_t *client_args = (ClientArgs_t *)malloc(sizeof(ClientArgs_t));
        if (!client_args)
        {
            fprintf(stderr, "线程专属 客户端信息结构体 内存分配失败\n");
            close(connect_fd);
            continue;
        }

        // 配置客户端信息结构体, 将信息传递到子线程
        client_args->addr_len = client_len;
        client_args->sock_fd = connect_fd;
        client_args->socket_addr = client;
        memset(client_args->receive_msgBuffer, 0, BUF_SIZE);

        pthread_t ReceivedFromClient_thread; // 用于接收客户端传回的信息 新线程的TID
        // 创建接收线程  并将客户端IP信息结构体信息传入线程
        if (pthread_create(&ReceivedFromClient_thread, NULL, ReceivedFromClient, (void *)client_args) != 0)
        {
            fprintf(stderr, "创建接收线程错误, errno:%d, %s\n", errno, strerror(errno));
            close(connect_fd); // 关闭对客户端套接字
            free(client_args);
            continue; // 进入下一个请求
        }
        pthread_detach(ReceivedFromClient_thread); // 线程分离 主要目的是使得线程在终止时能够自动释放其占用的资源,而不需要其他线程显式地调用 pthread_join 来清理它。
        /************END*************/

        /************第五步: 主线程发送数据给客户端************/
        char buffer[BUF_SIZE]; // 存放要发的数据缓冲区
        while (!stop)
        {
            if (client_live_flag == 0)
            {
                break;
            }
            // 清理缓冲区
            memset(buffer, 0x0, sizeof(buffer));
            // 接收用户输入的字符串数据
            printf("请输入要发送的字符串(输入exit退出服务器程序):");
            fgets(buffer, sizeof(buffer), stdin);
            // 将用户输入的数据发送给服务器
            if (send(connect_fd, buffer, strlen(buffer), 0) < 0)
            {
                perror("发送错误:");
                break;
            }
            // 输入了"exit",退出循环
            if (strncmp(buffer, "exit", 4) == 0)
            {
                close(connect_fd);
                printf("你输入了exit, 服务器端程序结束\n");
                break;
            }
        }
    }
    close(tcp_socket);
    printf("服务器程序结束\n");
    return 0;
}

客户端

/**
 * @file name : tcp_client.c
 * @brief     : 从终端输入服务器IP 端口号  与服务器建立TCP连接 并相互通信
 * @author    : RISE_AND_GRIND@163.com
 * @date      : 2024年6月5日
 * @version   : 1.0
 * @note      : 编译命令 cc tcp_client.c -o tcp_client.out -pthread
 *              运行:./tcp_client.out 1xx.7x.1x.2xx 50001
 *              输入 exit 退出客户端
 * CopyRight (c)  2023-2024   RISE_AND_GRIND@163.com   All Right Reseverd
 */

#include <stdio.h>
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#define BUF_SIZE 1024 // 缓冲区大小(字节)
/**
 * @name      receive_from_server
 * @brief     接收线程函数, 用于处理C-->S的信息
 * @param     arg 线程例程参数, 传入服务器的网络信息
 * @note
 */
void *receive_from_server(void *arg)
{
    int tcp_socket_fd = *(int *)arg;
    char buf[BUF_SIZE];
    while (1)
    {
        memset(buf, 0, sizeof(buf));
        ssize_t bytes_received = recv(tcp_socket_fd, buf, sizeof(buf) - 1, 0);
        if (bytes_received > 0)
        {
            printf("从服务器接收到数据: %s\n", buf);
        }
        else if (bytes_received == 0)
        {
            printf("服务器断开连接\n");
            break;
        }
        else
        {
            perror("接收错误");
            break;
        }
    }
    return NULL;
}
// 运行客户端可执行文件 ./xxx 目标服务器地址 服务器端口
int main(int argc, char const *argv[])
{
    // 检查参数有效性
    if (argc != 3)
    {
        fprintf(stderr, "从终端输入的参数无效, errno:%d,%s\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }

    /************第一步: 打开套接字, 得到套接字描述符************/
    int tcp_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (0 > tcp_socket_fd)
    {
        fprintf(stderr, "tcp socket error,errno:%d,%s\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }
    /************END*************/

    /************第二步: 调用connect连接远端服务器************/
    struct sockaddr_in server_addr = {0}; // 服务器IP信息结构体
    // 配置服务器信息结构体
    server_addr.sin_family = AF_INET;                 // 协议族,是固定的
    server_addr.sin_port = htons(atoi(argv[2]));      // 服务器端口,必须转换为网络字节序
    server_addr.sin_addr.s_addr = inet_addr(argv[1]); // 服务器地址 "192.168.64.xxx"
    int ret = connect(tcp_socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (0 > ret)
    {
        perror("连接错误:");
        close(tcp_socket_fd);
        exit(EXIT_FAILURE);
    }
    printf("服务器连接成功...\n\n");
    /************END*************/

    /************第三步: 向服务器发送数据************/

    // 创建接收线程
    pthread_t recv_thread;
    if (pthread_create(&recv_thread, NULL, receive_from_server, &tcp_socket_fd) != 0)
    {
        perror("线程创建失败");
        close(tcp_socket_fd);
        exit(EXIT_FAILURE);
    }
    pthread_detach(recv_thread); // 线程分离 主要目的是使得线程在终止时能够自动释放其占用的资源,而不需要其他线程显式地调用 pthread_join 来清理它。
    /* 向服务器发送数据 */
    char buf[BUF_SIZE]; // 数据收发缓冲区
    for (;;)
    {
        // 清理缓冲区
        memset(buf, 0, sizeof(buf));
        // 接收用户输入的字符串数据
        printf("请输入要发送的字符串: ");
        if (fgets(buf, sizeof(buf), stdin) == NULL)
        {
            perror("fgets error");
            break;
        }

        // 将用户输入的数据发送给服务器
        ret = send(tcp_socket_fd, buf, strlen(buf), 0);
        if (0 > ret)
        {
            perror("发送错误:");
            break;
        }

        // 输入了"exit",退出循环
        if (0 == strncmp(buf, "exit", 4))
            break;
    }
    /************END*************/
    close(tcp_socket_fd);
    printf("客户端程序结束\n");
    exit(EXIT_SUCCESS);
    return 0;
}

posted @ 2024-06-06 00:01  舟清颺  阅读(156)  评论(0编辑  收藏  举报