【网络编程】用于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 = 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) { 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测试,记录如下:
​ 服务器端的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3rgsfgsy-1661923478845)(data:image/gif;base64,r0lgodlhaqabapabap///waaach5baekaaaalaaaaaabaaeaaaicraeaow==)]编辑
​ 以下是客户端对应的接收的3组echo请求数据:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzv1ravq-1661923478849)(data:image/gif;base64,r0lgodlhaqabapabap///waaach5baekaaaalaaaaaabaaeaaaicraeaow==)]编辑
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k63yvffw-1661923478850)(data:image/gif;base64,r0lgodlhaqabapabap///waaach5baekaaaalaaaaaabaaeaaaicraeaow==)]编辑
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cvqfinim-1661923478852)(data:image/gif;base64,r0lgodlhaqabapabap///waaach5baekaaaalaaaaaabaaeaaaicraeaow==)]编辑
​ 经对比可以发现,echo的数据与客户端发送的原始请求数据是一致的,证明echo-server运行是完全没有问题的。
​ 综述,灵活使用好这个echo服务器可以高效地对客户端的网络做一些排查工作,比如通过客户端去连接这个echo服务器,就可以很快知道客户端当前的网络环境是不是畅通的?数据发送和数据接收功能是否是正常的?还可以大致分析出客户端网络通讯的瓶颈,究竟是连接耗时还是数据发送耗时,还是数据接收耗时,具体的耗时大致是什么级别,等等。
​ 话又说回来,文中的echo服务器代码毕竟仅仅是测试代码,仅用于应对一些网络测试功能;如果真要应用在正式的生产环境,那其中的个别代码还需要进一步斟酌、优化,这部分的工作就交给有心的读者吧。如果读者在阅读文本的过程中,发现有纰漏之处,可以随时与笔者联系,欢迎您的指正。谢谢。


聚焦主业拓展,发力巴西市场,江波龙收购SMART Brazil 81%股权正式完成交割
如何延长恒温恒湿试验箱的使用寿命
可穿戴珠宝化:中兴思秸发布首款智能戒指
Intersil推出针对应用处理器、GPU、FPGA以及系统电源的最小尺寸和最高效率PMIC
LG伊诺特开发出车载灵活的立体照明,可实现五面发光
【网络编程】用于echo回显测试的TCP服务器的设计
关系数据库是什么?
ADSL1110引脚功能、特点及内部结构介绍
一加5什么时候上市?一加5最新消息:一加5配置、性能、外观抢先看,骁龙835+8G内存!
三星SDHCPRO32G评测 可谓是不可多得的神作
Modbus网关模块方案
工业物联网应用:工程机械制造工厂数据采集系统
一种简化数据和改进控制的高性能方法
新品速递丨Mini-Fit Max 连接器
74LS245总线收发器介绍
三星Galaxy Fold将于9月份上市搭载骁龙855处理器支持5G网络
肖特基二极管型号和稳压管型号对应
快讯!飞腾3款CPU入选“中国品牌日国货新品”
慧尔视获得第十二届“2022交通感知优秀产品奖”
零基础入门FPGA,FPGA学习重点