【網絡編程】用於echo回顯測試的TCP服務器的設計

        筆者在工作中,常常接觸到網絡通訊相關的內容,經常需要着手解決一些網絡通訊相關的疑難雜症。排查網絡問題的時候,往往需要藉助一些工具,而很多時候自己想要的功能,網上又未能找到匹配度高的exe工具。無奈之下,有的時候就不能不自己碼代碼,寫一些【爲我所用】的測試代碼,來幫助自己完成問題的排查。

        本文主要介紹一個TCP服務器端的測試程序,它的主要功能是:接收TCP客戶端的連接,當收到客戶端發送的消息後,立刻給客戶端回覆收到的消息;這個功能,通俗來講,就叫【回顯】。別看它很簡單,但是在實際排查網絡問題時,確實非常地有效。

        通過本文的閱讀,你將瞭解到以下內容:

  • TCP客戶端/服務器代碼邏輯的剖析
  • TCP服務器端如何獲取客戶端的IP地址和端口信息
  • TCP回顯測試服務器的使用和驗證

        鑑於筆者主要集中在Linux環境編程,以下所有講解都是基於Linux環境;如在Windows環境下編程,可能需要更改相應的網絡編程API,修改後的功能讀者自行驗證。


TCP客戶端/服務器代碼邏輯的剖析


        在Linux環境下,要實現網絡通訊,我們一般採用的都是socket編程;但是,Linux環境下的socket編程是一個大類,並不僅僅只有網絡編程纔是socket編程,有一種叫Unix Domain Socket編程,它也叫socket編程。只不過它一般不用於遠程的網絡通訊,而是用於本地(當前主機環境內)進程之間的通訊。曾經就因爲這個問題,筆者在一次面試中,就被見多識廣的面試官DISS了一番,希望大家也補補這方面的知識。以下部分講述的主要是基於局域網或廣域網的網絡socket編程。

        在網絡socket編程中,會有2種不同的【身份】:客戶端和服務器。【客戶端】指的是,網絡連接的發起方,作爲網絡處理的請求方,向對端請求某種服務。【服務器】指的是,網絡連接的被動連接方,一般它不能主動連接別人,只能監聽客戶端的連接,待它收到客戶端的服務請求後,會對客戶端的服務請求做出響應;通常服務器的運行模式是一個服務器可對應N個客戶端。

        在TCP socket 網絡編程中,客戶端的代碼邏輯一般是:

【 socket -> bind -> connect -> send -> recv -> close 】
socket:創建一個socket套接字,用於執行此次網絡連接
bind:將服務器的信息(主要是ip和端口)與創建的socket綁定
connect: 向服務器發起網絡連接請求
send: 將客戶端的數據發送到服務器端
recv: 接收服務器迴應的處理數據
close: 關閉socket套接字,釋放對應的系統資源

        對應的,TCP服務器的代碼邏輯一般是:

【 socket -> bind -> listen -> accept -> recv -> send -> close 】
socket:創建一個socket套接字,用於執行此次服務器的網絡服務
bind:將當前需要創建的服務器的信息(主要是ip和端口)與創建的socket綁定,該ip和端口就是客戶端bind操作時需要用到的ip和端口
listen: 設置socket套接字執行監聽,此處可以設置服務器最多能同時接收多少個客戶端的連接
accept: 接受客戶端的連接請求,此處對應的就是客戶端的connect操作
recv: 接收客戶端發送的請求數據
send: 將處理完的請求數據發送到客戶端
close: 關閉socket套接字,釋放對應的系統資源

        瞭解了TCP客戶端和服務器的基本代碼邏輯後,我們直接附上tcp-echo-服務器的測試代碼:tcp_server_echo.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define MAX_CLINET_NUM 10 /** 最大客戶端連接數,可根據實際情況增減 */

/** 使用hexdump格式打印數據的利器 */
static void hexdump(const char *title, const void *data, unsigned int len)
{
    char str[160], octet[10];
    int ofs, i, k, d;
    const unsigned char *buf = (const unsigned char *)data;
    const char dimm[] = "+------------------------------------------------------------------------------+";

    printf("%s (%d bytes)\n", title, len);
    printf("%s\r\n", dimm);
    printf("| Offset  : 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   0123456789ABCDEF |\r\n");
    printf("%s\r\n", dimm);

    for (ofs = 0; ofs < (int)len; ofs += 16) {
        d = snprintf( str, sizeof(str), "| %08x: ", ofs );
        for (i = 0; i < 16; i++) {
            if ((i + ofs) < (int)len)
                snprintf( octet, sizeof(octet), "%02x ", buf[ofs + i] );
            else
                snprintf( octet, sizeof(octet), "   " );

            d += snprintf( &str[d], sizeof(str) - d, "%s", octet );
        }
        d += snprintf( &str[d], sizeof(str) - d, "  " );
        k = d;

        for (i = 0; i < 16; i++) {
            if ((i + ofs) < (int)len)
                str[k++] = (0x20 <= (buf[ofs + i]) &&  (buf[ofs + i]) <= 0x7E) ? buf[ofs + i] : '.';
            else
                str[k++] = ' ';
        }

        str[k] = '\0';
        printf("%s |\r\n", str);
    }

    printf("%s\r\n", dimm);
}

/** 獲取客戶端的ip和端口信息 */
static int get_clinet_ip_port(int sock, char *ip_port, int len, int *port)
{
    struct sockaddr_in sa;
    int sa_len;
	
    sa_len = sizeof(sa);
    if(!getpeername(sock, (struct sockaddr *)&sa, &sa_len)) {
        *port = ntohs(sa.sin_port);
        snprintf(ip_port, len, "%s", inet_ntoa(sa.sin_addr));
    }
    return 0;
}


/** 服務器端處理客戶端請求數據的線程入口函數 */
static void *client_deal_func(void* arg)
{
    nt client_sock = *(int *)arg;
	
    while(1) {  
        char buf[4096];
        int ret;
		
        memset(buf,'\0',sizeof(buf));
        ret = read(client_sock,buf,sizeof(buf)); /* 讀取客戶端發送的請求數據 */
        if (ret <= 0) {
            break; /* 接收出錯,跳出循環 */
        }

        hexdump("server recv:", buf, ret);
        ret = write(client_sock, buf, ret); /* 將收到的客戶端請求數據發送回客戶端,實現echo的功能 */
        if( ret < 0) {
            break; /* 發送出錯,跳出循環 */
        }
    }
	
    close(client_sock);
}

/** 服務器主函數入口,接受命令參數輸入,指定服務器監聽的端口號 */
int main(int argc, char **argv)
{
    int ret;
    int ser_port = 0;
    int ser_sock = -1;
    int client_sock = -1;
    struct sockaddr_in server_socket;
    struct sockaddr_in socket_in;
    pthread_t thread_id;  
    int val = 1;
	
    /* 命令行參數的簡單判斷和help提示 */
    if(argc != 2) {
        printf("usage: ./client [port]\n");
        ret = -1;
        goto exit_entry;
    }
	
    /* 讀取命令行輸入的服務器監聽的端口 */
    ser_port = atoi(argv[1]);
    if (ser_port <=0 || ser_port >= 65536) {
        printf("server port error: %d\n", ser_port);
        ret = -2;
        goto exit_entry;
    }
	
    /* 創建socket套接字 */
    ser_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(ser_sock < 0) {
        perror("socket error");
        return -3;
    }
		
    /* 設置socket屬性,使得服務器使用的端口,釋放後,別的進程立即可重複使用該端口 */
    ret = setsockopt(ser_sock, SOL_SOCKET,SO_REUSEADDR, (void *)&val, sizeof(val));
    if(ret == -1) {
        perror("setsockopt");
        return -4;
    }
	
    bzero(&server_socket, sizeof(server_socket));
    server_socket.sin_family = AF_INET;
    server_socket.sin_addr.s_addr = htonl(INADDR_ANY); //表示本機的任意ip地址都處於監聽
    server_socket.sin_port = htons(ser_port);
	
    /* 綁定服務器信息 */
    if(bind(ser_sock, (struct sockaddr*)&server_socket, sizeof(struct sockaddr_in)) < 0) {
        perror("bind error");
        ret = -5;
        goto exit_entry;
    }
	
    /* 設置服務器監聽客戶端的最大數目 */
    if(listen(ser_sock, MAX_CLINET_NUM) < 0) { 
        perror("listen error");
        ret = -6;
        goto exit_entry;
    }
	
    printf("TCP server create success, accepting clients ...\n");
    for(;;) { /* 循環等待客戶端的連接 */
        char buf_ip[INET_ADDRSTRLEN];		
        socklen_t len = 0;
        client_sock = accept(ser_sock, (struct sockaddr*)&socket_in, &len);
        if(client_sock < 0) {
            perror("accept error");
            ret = -7;
            continue;
        }		
        
        {
            char client_ip[128];
            int client_port;
            get_clinet_ip_port(client_sock, client_ip, sizeof(client_ip), &client_port);
            /* 打印客戶端的ip和端口信息 */
            printf("client connected [ip: %s, port :%d]\n", client_ip, client_port);
        }
		
        /* 使用多線程的方式處理客戶端的請求,每接收一個客戶端連接,啓動一個線程處理對應的數據 */
        pthread_create(&thread_id, NULL, (void *)client_deal_func, (void *)&client_sock);  
        pthread_detach(thread_id); 
    }
	
exit_entry:
    if (ser_sock >= 0) {
        close(ser_sock); /* 程序退出前,釋放socket資源 */
    }
    return 0;
}

TCP服務器端如何獲取客戶端的IP地址和端口信息


        如上的測試代碼中,有這麼一個函數:

/** 獲取客戶端的ip和端口信息 */
static int get_clinet_ip_port(int sock, char *ip_port, int len, int *port)
{
    struct sockaddr_in sa;
    int sa_len;
	
    sa_len = sizeof(sa);
    if(!getpeername(sock, (struct sockaddr *)&sa, &sa_len)) {
        *port = ntohs(sa.sin_port);
        snprintf(ip_port, len, "%s", inet_ntoa(sa.sin_addr));
    }
    return 0;
}

        get_clinet_ip_port函數是在服務器成功接受了客戶端的連接之後被調用,sock是該通訊鏈路對應的socket通道,函數內部通過getpeername接口,取得對方(客戶端)的地址信息,存放在結構體sa中;接着使用ntohs將sa中的端口信息轉成int類型,通過函數的入參port傳遞出去;使用inet_ntoa將sa中的ip地址信息轉成字符串類型,通過函數的入參ip傳遞出去。這樣,函數的調用者,通過ip和port變量就取得了客戶端的ip和端口信息了。下面會給出,這個函數成功調用後,打印出的客戶端信息範例。


TCP回顯測試服務器的使用和驗證


        有了tcp-server-echo的代碼,我們就可以執行編譯、測試了。編譯程序,在Linux控制檯如下輸入:

gcc tcp_server_echo.c -o tcp_server_echo -lpthread

        加上-lpthread表示鏈接多線程庫,因爲程序中用到了多線程操作。正常編譯成功後,就可以在當前工程目錄看到tcp_server_echo文件的存在,這個就是我們編譯出來的可執行文件。

        編譯成功後,使用以下命令啓動服務器,其中6210表示啓動服務器需要監聽的端口號;注意,啓動服務器時一定要輸入監聽的端口號,否則啓動會報錯。

./tcp_server_echo 6210

        以下是筆者使用該測試服務器對客戶端的連接做echo測試,記錄如下:

        服務器端的輸出:

        以下是客戶端對應的接收的3組echo請求數據:

        經對比可以發現,echo的數據與客戶端發送的原始請求數據是一致的,證明echo-server運行是完全沒有問題的。


        綜述,靈活使用好這個echo服務器可以高效地對客戶端的網絡做一些排查工作,比如通過客戶端去連接這個echo服務器,就可以很快知道客戶端當前的網絡環境是不是暢通的?數據發送和數據接收功能是否是正常的?還可以大致分析出客戶端網絡通訊的瓶頸,究竟是連接耗時還是數據發送耗時,還是數據接收耗時,具體的耗時大致是什麼級別,等等。

        話又說回來,文中的echo服務器代碼畢竟僅僅是測試代碼,僅用於應對一些網絡測試功能;如果真要應用在正式的生產環境,那其中的個別代碼還需要進一步斟酌、優化,這部分的工作就交給有心的讀者吧。如果讀者在閱讀文本的過程中,發現有紕漏之處,可以隨時與筆者聯繫,歡迎您的指正。謝謝。

版權申明:本文爲博主原創文章,轉載請註明出處!https://blog.csdn.net/szullc/article/details/84865006

原創作者:李路昌

電子郵箱:[email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章