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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...