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

来源:https://www.cnblogs.com/zqingyang/p/18234204
-Advertisement-
Play Games

V1.0 2024年6月5日 發佈於博客園 目錄理論代碼伺服器端客戶端 理論 伺服器端先運行, 能夠接收來自任何地方的多個客戶端發起的指向特定埠(這裡是50002)的TCP請求, 並和客端建立穩定的TCP連接. 沒有連接請求時等待, 有連接後先來後到的原則, 依次服務, 能夠相互通信. 當客戶端結 ...


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    : [email protected]
 * @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   [email protected]   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    : [email protected]
 * @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   [email protected]   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;
}

本文來自博客園,作者:舟清颺,轉載請註明原文鏈接:https://www.cnblogs.com/zqingyang/p/18234204


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 大家好,我是老馬。很高興遇到你。 我們希望實現最簡單的 http 服務信息,可以處理靜態文件。 如果你想知道 servlet 如何處理的,可以參考我的另一個項目: 手寫從零實現簡易版 tomcat minicat 手寫 nginx 系列 如果你對 nginx 原理感興趣,可以閱讀: 從零手寫實 ...
  • 源碼: <?php error_reporting(0); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header("HTTP/1.1 405 Method Not Allowed"); exit(); } else { if (!isset($_PO ...
  • 在 Mac 上安裝多個 Python 版本可通過幾種不同方法實現。 1 Homebrew 1.1 安裝 Homebrew 若安裝過,跳過該步。 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/ ...
  • 目錄一、背景介紹二、爬蟲代碼2.1 展示爬取結果2.2 爬蟲代碼講解三、可視化代碼3.1 讀取數據3.2 數據清洗3.3 可視化3.3.1 IP屬地分析-柱形圖3.3.2 評論時間分析-折線圖3.3.3 點贊數分佈-箱線圖3.3.4 評論內容-情感分佈餅圖3.3.5 評論內容-詞雲圖四、技術總結五、 ...
  • 大家好,我是R哥。 最近,R哥分享了幾個特別有意思的面試輔導成功案例: 35K*14 薪入職了,這公司只要不裁員,我能一直呆下去。。 幹了 2 年多 Java 外包,終於脫離了! 輔導一周,連拿 3 個 Offer! 說到 985 學歷,找工作應該不算是什麼難事吧?何況還是 985 碩士? 這個學歷 ...
  • RuoYi-activiti —— 基於若依、Activiti 6.0,集流程設計、流程部署、流程執行、任務辦理、流程監控於一體的開源工作流開發平臺。 ...
  • 一、背景介紹 1.1 爬取目標 用python開發了一個抖音爬蟲採集軟體,可自動按博主抓取其已發佈視頻數據。 為什麼有了源碼還開發界面軟體呢?方便不懂編程代碼的小白用戶使用,無需安裝python,無需改代碼,雙擊打開即用! 軟體界面截圖: 爬取結果截圖: 結果截圖1: 結果截圖2: 結果截圖3: 以 ...
  • 寫在前面 這是PB案例學習筆記系列文章的第3篇,該系列文章適合具有一定PB基礎的讀者。 通過一個個由淺入深的編程實戰案例學習,提高編程技巧,以保證小伙伴們能應付公司的各種開發需求。 文章中設計到的源碼,小凡都上傳到了gitee代碼倉庫https://gitee.com/xiezhr/pb-proje ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...