笔者在工作中,常常接触到网络通讯相关的内容,经常需要着手解决一些网络通讯相关的疑难杂症。排查网络问题的时候,往往需要借助一些工具,而很多时候自己想要的功能,网上又未能找到匹配度高的exe工具。无奈之下,有的时候就不能不自己码代码,写一些【为我所用】的测试代码,来帮助自己完成问题的排查。
本文主要介绍一个TCP服务器端的测试程序,它的主要功能是:接收TCP客户端的连接,当收到客户端发送的消息后,立刻给客户端回复收到的消息;这个功能,通俗来讲,就叫【回显】。别看它很简单,但是在实际排查网络问题时,确实非常地有效。
通过本文的阅读,你将了解到以下内容:
鉴于笔者主要集中在Linux环境编程,以下所有讲解都是基于Linux环境;如在Windows环境下编程,可能需要更改相应的网络编程API,修改后的功能读者自行验证。
在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
#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;
}
如上的测试代码中,有这么一个函数:
/** 获取客户端的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-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]