【网络编程】用于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套接字,释放对应的系统资源

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6R5slz09-1661923478821)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 对应的,TCP服务器的代码逻辑一般是:

【 socket -> bind -> listen -> accept -> recv -> send -> close 】
socket:创建一个socket套接字,用于执行此次服务器的网络服务
bind:将当前需要创建的服务器的信息(主要是ip和端口)与创建的socket绑定,该ip和端口就是客户端bind操作时需要用到的ip和端口
listen: 设置socket套接字执行监听,此处可以设置服务器最多能同时接收多少个客户端的连接
accept: 接受客户端的连接请求,此处对应的就是客户端的connect操作
recv: 接收客户端发送的请求数据
send: 将处理完的请求数据发送到客户端
close: 关闭socket套接字,释放对应的系统资源

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-erGqy5UU-1661923478827)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 了解了TCP客户端和服务器的基本代码逻辑后,我们直接附上tcp-echo-服务器的测试代码:tcp_server_echo.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PtxtqEBB-1661923478828)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]


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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s92JFClt-1661923478839)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RgCENij3-1661923478841)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 加上-lpthread表示链接多线程库,因为程序中用到了多线程操作。正常编译成功后,就可以在当前工程目录看到tcp_server_echo文件的存在,这个就是我们编译出来的可执行文件。

​ 编译成功后,使用以下命令启动服务器,其中6210表示启动服务器需要监听的端口号;注意,启动服务器时一定要输入监听的端口号,否则启动会报错。

./tcp_server_echo 6210

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RpaaPcOC-1661923478843)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 以下是笔者使用该测试服务器对客户端的连接做echo测试,记录如下:

​ 服务器端的输出:

TCP

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3rgsFgSy-1661923478845)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

​ 以下是客户端对应的接收的3组echo请求数据:

TCP

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JZv1RaVq-1661923478849)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

TCP

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k63YVfFw-1661923478850)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

TCP

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cVqFINIm-1661923478852)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

​ 经对比可以发现,echo的数据与客户端发送的原始请求数据是一致的,证明echo-server运行是完全没有问题的。


​ 综述,灵活使用好这个echo服务器可以高效地对客户端的网络做一些排查工作,比如通过客户端去连接这个echo服务器,就可以很快知道客户端当前的网络环境是不是畅通的?数据发送和数据接收功能是否是正常的?还可以大致分析出客户端网络通讯的瓶颈,究竟是连接耗时还是数据发送耗时,还是数据接收耗时,具体的耗时大致是什么级别,等等。

​ 话又说回来,文中的echo服务器代码毕竟仅仅是测试代码,仅用于应对一些网络测试功能;如果真要应用在正式的生产环境,那其中的个别代码还需要进一步斟酌、优化,这部分的工作就交给有心的读者吧。如果读者在阅读文本的过程中,发现有纰漏之处,可以随时与笔者联系,欢迎您的指正。谢谢。
 

 审核编辑:汤梓红
 
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分