TCP實現公網伺服器和內網客戶端一對多訪問(C語言實現)

舟清颺發表於2024-06-06

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;
}

相關文章