C語言透過socket實現TCP客戶端

crazy3min發表於2024-06-06

socket概念

​ 從wiki上了解,socket這個詞追溯到 1971 年 RFC 147 的釋出。

​ 目前我的理解:常用於指作業系統提供的 API,該 API 允許使用 TCP、UDP 進行連線,但不僅限於 TCP、UDP 協議。

實現目的

利用系統提供函式介面,透過C語言實現對TCP 伺服器(IP地址)的連線,以及收發資料。

實現過程

1、socket(2) 建立套接字

2、connect(2) 連線伺服器。伺服器已開啟,否則會直接返回錯誤。

3、send(2) 向伺服器傳送資料。連線成功後,即可與伺服器通訊。

4、recv(2) 接收伺服器傳送過來的資料。

5、close(2) 關閉套接字。

實現程式碼

/****************************************************************************
 *
 * file name: mytcp_client.c
 * author   : crazy3min@outlook.com
 * date     : 2024-06-05
 * function : TCP協議的客戶端操作。
 * note     :
 *              測試編譯指令: gcc ./src/mytcp_client.c ./src/mytime.c -o ./bin/mytcp_client -I ./include
 *              透過命令列輸入伺服器ip和埠,示例:./bin/mytcp_client IP PORT
 *
 * CopyRight (c)   2024   crazy3min@outlook.com   Right Reseverd
 *
 ****************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdbool.h>
#include <pthread.h>
#include <netinet/in.h>
#include "mytime.h" //時間標頭檔案

#define DEBUG // 開啟除錯模式

/******************************* 全域性變數 START *******************************/
char timebuf[128]; // 時間輸出緩衝區
/******************************* 全域性變數 END *******************************/

/****************************************************************************
 *
 * function name     : tcp_v4_hton
 * function          : 將ipv4伺服器的資訊從本地位元組序轉換為網路位元組序,並儲存在destinfo指標下。
 * parameter         :
 *                      @destinfo: 儲存轉換後的資訊指標。
 *                      @address: 需要轉換的點分十進位制IPv4地址,例如 "192.168.5.1"
 *                      @port: 需要轉換的埠,例如:60000
 *
 * return value      : None
 * note              : None
 *
 * author            : crazy3min@outlook.com
 * date              : 2024-06-06
 * version           : V1.0
 * revision history  : None
 *
 ****************************************************************************/
void tcp_v4_hton(struct sockaddr_in *destinfo, const char *address, const int port)
{

    destinfo->sin_family = AF_INET;                 // 協議,AF_INET代表IPV4協議
    destinfo->sin_port = htons(port);               // 伺服器埠,必須將目標埠轉為網路位元組序(大端)
    destinfo->sin_addr.s_addr = inet_addr(address); // 伺服器ip,必須將目標ip轉為網路位元組序(大端)
}

/****************************************************************************
 *
 * function name     : tcp_v4_connect
 * function          : 連線IPv4 TCP伺服器
 * parameter         :
 *                      @socketfd: socket指標。
 *                      @destinfo: 儲存 IPv4 TCP伺服器資訊的指標
 *
 * return value      : 成功返回true,失敗返回false
 * note              : None
 *
 * author            : crazy3min@outlook.com
 * date              : 2024-06-06
 * version           : V1.0
 * revision history  : None
 *
 ****************************************************************************/
bool tcp_v4_connect(int *socketfd, struct sockaddr_in *destinfo)
{
    // 建立ipv4 TCP 通訊端點
    *socketfd = socket(destinfo->sin_family, SOCK_STREAM, 0);
    if (-1 == *socketfd)
    {
        fprintf(stderr,
                "[%s] [%s] 建立ipv4 TCP 通訊端點,Error code: %d, Error message: %s\n",
                __FILE__,
                __func__,
                errno,
                strerror(errno));
        return false;
    }

    // 請求連線 tcp伺服器
    if (-1 == connect(*socketfd, (const struct sockaddr *)destinfo, sizeof(struct sockaddr_in)))
    {
        fprintf(stderr,
                "[%s] [%s] 連線ipv4 TCP 通訊端點失敗,Error code: %d, Error message: %s\n",
                __FILE__,
                __func__,
                errno,
                strerror(errno));
        return false;
    }

// 成功連線
#ifdef DEBUG
    time_format(timebuf, sizeof(timebuf), "%Y年%m月%d日 %H:%M:%S");
    printf("[DEBUG][%s]成功連線伺服器 \n", timebuf);
#endif
    return true;
}

/****************************************************************************
 *
 * function name     : tcp_v4_send
 * function          : 向IPv4 TCP伺服器傳送一條資料
 * parameter         :
 *                      @socketfd: 已連線伺服器的socket控制代碼
 *                      @buf: 儲存待傳送資料的緩衝區指標。
 *                      @bufsize: 資料的大小,單位位元組。
 *
 * return value      : 成功返回true,失敗返回false
 * note              :
 *                      必須先使用tcp_v4_connect()連線伺服器後再使用。
 *
 * author            : crazy3min@outlook.com
 * date              : 2024-06-06
 * version           : V1.0
 * revision history  : None
 *
 ****************************************************************************/
bool tcp_v4_send(const int socketfd, const char *buf, const int bufsize)
{
    if (bufsize != send(socketfd, buf, bufsize, 0))
    {
#ifdef DEBUG
        time_format(timebuf, sizeof(timebuf), "%Y年%m月%d日 %H:%M:%S");
        printf("[DEBUG][%s] 傳送 [%s] 失敗!!!\n", timebuf, buf);
#endif
        return false;
    }

#ifdef DEBUG
    time_format(timebuf, sizeof(timebuf), "%Y年%m月%d日 %H:%M:%S");
    printf("[DEBUG][%s] 成功傳送 [%s] \n", timebuf, buf);
#endif
    return true;
}

/****************************************************************************
 *
 * function name     : tcp_client_recv
 * function          : 執行緒任務,連線TCP IPv4伺服器後,接收伺服器發來的資訊並輸出。
 * parameter         :
 *                      @arg: 已連線伺服器的socket控制代碼指標
 *
 * return value      : None
 * note              :
 *                      必須先使用tcp_v4_connect()連線伺服器後再使用。
 *
 * author            : crazy3min@outlook.com
 * date              : 2024-06-06
 * version           : V1.0
 * revision history  : None
 *
 ****************************************************************************/
void *tcp_client_recv(void *arg)
{
    int socketfd = *((int *)arg); // 轉換透過引數傳入socket套接字控制代碼
    char buffer[512] = {0};       // 接收資料緩衝區

    // 迴圈阻塞等待伺服器傳送的資料
    while (1)
    {
        if (0 == (recv(socketfd, buffer, sizeof(buffer), 0)))
        {
            // 伺服器終止連線,結束程式
            time_format(timebuf, sizeof(timebuf), "%Y年%m月%d日 %H:%M:%S");
            printf("[%s] 伺服器已經終止連線\n", timebuf);
            close(socketfd);
            exit(EXIT_SUCCESS);
        }
        time_format(timebuf, sizeof(timebuf), "%Y年%m月%d日 %H:%M:%S");
        printf("[%s]收到伺服器發來的資訊:[%s]\n", timebuf, buffer);
        bzero(buffer, sizeof(buffer)); // 清空快取
    }
}

int main(int argc, char const *argv[])
{
    // 透過終端傳入伺服器的資訊
    if (3 != argc)
    {
        printf("引數無效,請輸入 [./xxx IP PORT] 執行\n");
        return -1;
    }

    int socketfd;                // 建立套接字
    char buffer[512] = {0};      // 傳送資料緩衝區
    struct sockaddr_in destinfo; // 定義IPv4 地址和埠的結構體 變數儲存服務端資訊

    tcp_v4_hton(&destinfo, argv[1], atoi(argv[2])); // 轉換為網路位元組序

    // 連線伺服器
    if (!(tcp_v4_connect(&socketfd, &destinfo)))
        return -1;

    // 建立執行緒接收伺服器傳送的資訊
    pthread_t tcp_recv_task;
    if (0 != (pthread_create(&tcp_recv_task, NULL, tcp_client_recv, &socketfd)))
    {
        printf("********** 建立接收伺服器傳送的資訊執行緒失敗! **********\n");
    }
#ifdef DEBUG
    else
    {
        time_format(timebuf, sizeof(timebuf), "%Y年%m月%d日 %H:%M:%S");
        printf("[DEBUG][%s] 成功建立接收伺服器傳送的資訊執行緒 \n", timebuf);
    }
#endif

    // 需要傳送的資訊
    while (1)
    {
        printf("請輸入傳送的資料:\n");
        fgets(buffer, sizeof(buffer), stdin); // 標準輸入獲取資料
        buffer[strcspn(buffer, "\n")] = '\0'; // 替換換行符
        if (0 == strlen(buffer))
        {
            printf("********** 請輸入有效資訊 **********\n");
        }
        else if (!(tcp_v4_send(socketfd, buffer, strlen(buffer))))
        {
            time_format(timebuf, sizeof(timebuf), "%Y年%m月%d日 %H:%M:%S");
            printf("[%s] 伺服器已經終止連線\n", timebuf);
            close(socketfd);
            break;
        }
        else
        {
            bzero(buffer, sizeof(buffer)); // 清空快取
        }
    }

    return 0;
}

測試結果

C語言TCP客戶端測試

參考資訊

  • TCP/IP 簡介(第 4 部分)- 套接字和埠

衍生問題

  • 什麼是Berkeley Sockets

​ Berkeley 套接字是用於建立和使用套接字的行業標準應用程式程式設計介面 (API)。它最初被用作 Unix 作業系統的 API,後來被 TCP/IP 採用。

相關文章